Introduction

Find everything you might need to know about our product 21 Travel Rule here.

On-premise

21 Travel Rule is on-premise software that you can licence from 21 Analytics. At no time information is exposed or sent to 21 Analytics!

Multiprotocol

It is a multi-protocol FATF Travel Rule solution. Currently, the supported protocols are:

  • TRP
  • VerifyVASP
  • email

When other protocols gain traction in the market, we add them.

GUI

We offer a GUI for your compliance officer to handle incoming and outgoing Travel Rule messages. Managing VASP-to-VASP messages is done in the GUI. The same goes for address ownership proofs. The GUI is most suited for parties that have a limited number of transactions per day or parties who have integrated with our API and want a dashboard like functionality.

API

Additionally, a GraphQL API is available to integrate our software into your IT infrastructure. This requires more effort but can handle a large number of transactions. In addition, this allows for a high level of automation. For example, it is possible to automatically approve transactions that are below a certain threshold.

Deployment

We have documentation on the deployment of our software and subsequent upgrades. You are using Docker to deploy software in your infrastructure. Once a licence agreement is signed, we will give you access to our registry. We advise running at least two instances of our software - one for production and one for testing. Additionally, staging and failover instances should be run in an enterprise environment.

Mirror

We mirror some information you will find here. When we do, we will point to the authoritative source.

Questions

Please direct questions to info@21analytics.ch.

Deployment

Authoritative source

21 Travel Rule: Reference Deployment

21 Travel Rule is a software solution for FATF’s travel rule by 21 Analytics.

These instructions only work if you have a valid 21 Travel Rule license, which comes with a username and password to access our Docker Registry.

If you get stuck with the deployment instructions then please refer to our Troubleshooting section where common usage errors are clarified.

Hardware Requirements

We recommend the following minimum for operating the 21 Travel Rule software:

  • 2 CPU 2198 MHz
  • 2 GB RAM
  • 20 GB Disk

The hardware requirements depend the number of transactions you send & receive.

Software Requirements

To run 21 Travel Rule the following software needs to be installed:

  • Docker
  • Docker-Compose
  • git

Any operating system which can run Linux Docker containers will work.

Deployment with Docker-Compose

First, pull this git repository with

git clone https://gitlab.com/21analytics/21-travel-deployment.git

Second, login to our Docker Registry using the username / password that you have obtained from us by executing the following command:

docker login registry.21analytics.ch -u 'YourUsername' # single-quotes are important!

Then adjust the domain names in the Caddyfile to enable HTTPS. You probably want to commit your changes to the Caddyfile to simplify upgrades later:

git add Caddyfile
git commit -m "Caddyfile: Set domain"

After that, you can spin up your instance with the docker-compose file as shown below. The first time you run those commands the database access passwords are initialized. Therefore, you are free to choose those passwords. We recommend generating cryptographically secure passwords with your chosen key management solution.

After the first initialization, the environment variables still need to be set to successfully start the platform. The POSTGRES_PASSWORD can be omitted after the first initialization. Instead of exporting the environment variables you can use a .env file, see here. Also, the pg_data folder needs to be created where the application data is persisted.

export POSTGRES_PASSWORD=secret_password_1 # only required for init
export AUTOD_DB_PW=secret_password_2
export AOPD_DB_PW=secret_password_3
export TRPD_DB_PW=secret_password_4
export TRAVEL_LOG=info
mkdir pg_data
docker-compose up -d

You probably don't want to use the master branch. It contains the latest versions from our development branch which contain work-in-progress features.

Once the services are up and running, a user can be created by accessing the graphical user interface. After the first login, the user is redirected to the settings page where further details should be configured.

Logging

All our services emit log messages. The log level can be adjusted by setting the TRAVEL_LOG environment variable. Starting with the least verbose level the available log levels are:

  1. error
  2. warn
  3. info
  4. debug
  5. trace

where info is the default log level. TRAVEL_LOG=debug increases the logging level to debug.

Further, it is possible to selectively adjust the logging level for certain modules only (the module names can be obtained from existing logging output), e.g. to increase the logging level for HTTP traffic TRAVEL_LOG=tower_http::trace=debug should be set.

Putting all together the services can be run with a adjusted logging level for HTTP traffic like demonstrated in the following command.

TRAVEL_LOG=tower_http::trace=debug docker-compose up -d

Graphical User Interface

The graphical user interface can be accessed at port 3000 by default. It needs to be served from the root path /. If you decide to serve the graphical user interface on the public internet (not recommended) then a custom subdomain can be used to avoid collisions with other applications that also make use of the root path.

Upgrade

Move into the directory where you previously executed the install commands. Then do:

docker-compose down
git pull --rebase
docker-compose pull
docker-compose up -d

Please note that you likely need to point to a new Docker Compose file.

APIs exposed by Caddy reverse proxy

Here, we document the API endpoints that require to be publicly accessible. Our reference Caddy configuration in the Caddyfile already sets up everything accordingly. This is meant as a reference for firewall and WAF (web application firewall) configuration.

Travel Rule Protocol (TRP):
443 (HTTPS) at /transfers and /transfers/
TCP Incoming and Outgoing. This has to be accessible for your counterparty VASPs.

AOPP 443 (HTTPS) at /proofs/
TCP, incoming only. This has to be accessible for your customers.

Webinterface:
3000 (default setting)
TCP used internally only (by the VASP).
Make sure nobody can access this from outside of your organization.

Working with OpenShift/Kubernetes

Disclaimer: we don't offer support for deployments on OpenShift or Kubernetes due to the large diversity of possible architectures. However, we have found that our reference deployment offers a helpful guideline for deployments on OpenShift/Kubernetes. Therefore, we provide some hints below for how the reference deployment can be efficiently transformed for use on OpenShift or Kubernetes.

Converting the docker-compose.yml

You can use the kompose tool to convert the docker-compose.yml. Often, the generated files need some manual adjustments, e.g. you might want to remove the caddy reverse proxy service because you are already running a different solution.

Note: There is a known bug where kompose doesn't correctly translate environment variables from the docker-compose format to the kubernetes format. To make the environment variables work they either need to be wrapped using parentheses manually, e.g. $VAR into $(VAR) or this workaround can be applied.

Configuring your reverse proxy

You very likely have a reverse proxy running in your cluster already. The Caddyfile can be inspected to extract configuration details you need to apply for your reverse proxy.

Using an existing Postgres database

The init.db script can be inspected to extract the required configuration for Postgres (users, passwords, schemas, permissions). As a consequence the database connection URLs needs to be changed that are passed to the 21 travel rule services.

Troubleshooting

I'm seeing a Python traceback when running docker-compose

The output you see looks similar to

Traceback (most recent call last):
File "urllib3/connectionpool.py", line 670, in urlopen
File "urllib3/connectionpool.py", line 392, in _make_request
File "http/client.py", line 1255, in request
File "http/client.py", line 1301, in _send_request
File "http/client.py", line 1250, in endheaders
File "http/client.py", line 1010, in _send_output
File "http/client.py", line 950, in send
File "docker/transport/unixconn.py", line 43, in connect
FileNotFoundError: [Errno 2] No such file or directory

Those errors are usually encountered when the docker service is not running on your machine.

I'm using docker-compose with sudo and the environment variables are not set

sudo runs commands as a different user and doesn't preserve the original user's environment unless run using the --preserve-env flag. With that said, nowadays docker and docker-compose is commonly packaged such that it doesn't require sudo for execution. That's why our examples don't display the usage of sudo.

I'm using an .env file and the variables are not properly set

You've likely pasted the environment variables with a leading export command from our shell example snippet. Shell commands don't work in .env files and need to be omitted.

I'm getting the unhelpful 'Killed' error message

Your machine runs out of memory while starting the containers. Consider using a more powerful instance. 1GB is a minimum that is known to work.

GUI

Compliance officers are the targeted users of the GUI of 21 Travel Rule™. In their day to day operations, they will use the GUI to send out and review incoming Travel Rule messages. The GUI displays AOPP proofs as well for the compliance officer to inspect and export.

We expect low volume (but compliant!) customers to use the GUI. High volume customers might find it helpful as a dashboard of sorts.

Handling the management of known VASPs and the configuration of your VASP happens in the GUI as well.

In summary, the GUI exposes the following functionality:

  1. sending of Travel Rule messages
  2. reviewing of incoming Travel Rule messages
  3. own VASP configuration
  4. known counterparty VASP configuration
  5. AOPP proof inspection
  6. export of data

User Manual

This manual describes the GUI of 21 Travel Rule.

Setup

Once 21 Travel Rule is installed and used for the first time, the user is prompted to set a password and, optionally, configure counterparty VASPs.

User Name and Password

When the database doesn't contain a user, 21 Travel Rule prompts the compliance officer to create one.

Initial user creation

Initial user creation

You can un-hide the password fields. There are no password restrictions defined. Only one user is currently supported in the GUI (the API supports more). After entering the required user name and password, you are logged in immediately. The user name shows in the upper right corner of 21 Travel Rule.

Configure Your VASP

Once logged in, you need to configure your own VASP. You can configure four different sets of attributes:

  1. TRP
  2. VerifyVASP
  3. Email
  4. AOPP

Own VASP configuration

Configure your VASP

Fill out those you feel are relevant for your VASP.

Once you have saved a configuration, you are encouraged to configure counterparty VASPs.

TRP

TRP is our preferred protocol. First, fill out the name of your VASP and the associated LEI. Upon saving, a signing public key shows. If you wish to use message signing, you can give this to a counterparty VASP.

Email

If a VASP does not support any Travel Rule protocol, 21 Travel Rule can instead send an email with the relevant information. This makes you compliant with the Travel Rule nonetheless.

If you want to configure email, and we recommend you do that, you can do that on the 'Email' tab. The field values probably come from the IT department.

AOPP

On the tab 'AOPP', you can configure the callback URL. The end user's wallet uses this URL to submit a signature and new VA address. This URL must be publicly accessible.

Counterparty VASPs

Each incoming transaction needs to be associated with a counterparty VASP. If the incoming transaction's counterparty VASP is not configured, you will not see that transaction in the inbox.

Each counterparty VASP supports exactly one protocol. That protocol can be:

  1. TRP
  2. VerifyVASP
  3. Email

Add VASP

Add VASP

TRP

Three fields are required when configuring a VASP that supports TRP. Two are optional.

The name field is used to identify the VASP. For example, 'SDX'.

The API URL field tells 21 Travel Rule where the counterparty VASP runs its TRP protocol. You get this information from the counterparty VASP.

The LEI field is a unique identifier for the counterparty VASP, and they should provide it for you. The LEI registry is also public.

The Signing Public Key is optional. When filled out, the Message Signing TRP extension is going to be used when sending Travel Rule transactions. Both the originator and the beneficiary VASP need to configure this to work.

The last checkbox controls how 21 Travel Rule handles incoming Travel Rule transactions from this particular VASP. It will move transactions from this VASP immediately to the archive when checked.

Email

Add the name of the VASP and the email address. 21 Travel Rule will then send an email when you select this VASP when sending a Travel Rule transaction. It is required to have your email configured. See Email configuration.

Travel Rule

Regardless of protocol, inbound and sent/approved Travel Rule transactions end up in the inbox and archive, respectively.

Inbox

Transactions Inbox

The inbox contains inbound transactions of known VASP (and known VASPs only). A compliance officer can inspect the transactions and move them to the archive once he has dealt with them.

Inbox details

Transaction Details

The transaction details can be viewed by clicking anywhere on the transaction line. Besides the previously shown data, this shows all other relevant data concerning the transaction. For example, the amount is shown as the VA amount and the approximate dollar equivalent when the transaction was received. In addition, several items link to block explorers, Google Maps, and the public LEI registry for your convenience.

Transaction Status describes the status of the Travel Rule transaction, not the on-chain transaction. Transactions in the inbox have the status 'Pending'. In the archive, they are 'Executed'.

The Transaction Type tells you whether this transaction was sent or received.

Outbound Transactions

Sending Travel Rule transactions is the bread and butter of 21 Travel Rule.

The Travel Rule stipulates that beneficiary and originator data must be sent to the beneficiary VASP. Therefore, when clicking 'Send Transaction Notification' on the 'Transaction' tab, you are shown a pop up asking for the beneficiary data.

Beneficiary Information

Add beneficiary details

Add beneficiary information

The first and last names should come as no surprise. The originating user supplies these; he gets them from the beneficiary user. The wallet address is the beneficiary user's deposit address from his VASP.

Instead of a wallet address, a beneficiary VASP can give his user a Travel Address. This Travel Address is an encoded URL. When you give 21 Travel Rule a Travel Address, it decodes it. We then know the beneficiary VASP. Then we can ask the beneficiary VASP for a VA address. Using a Travel Address prevents asking the user to which VASP (if any) they will send their coins.

Virtual Asset

If the user does not supply a Travel Address, he should tell you to which VASP he is sending the coins. Fill that out under 'Select Beneficiary VASP'. Only to previously added VASPs can coins be sent (and received)!

Add virtual assert information

Add virtual asset information

The fields should be self-explanatory. You need to fill out the on-chain transaction ID. That means that the on-chain transaction should already have taken place. You only got the VA address in the previous step with the Travel Address. Do the on-chain transaction while leaving the web page with the second step open.

Originator Information

The final step is including the originator information. This information comes from your internal systems. As specified in IVMS101, you must supply either street name + building number or address line.

Add transaction originator

Add originator information

Once you click send, 21 Travel Rule sends the information to the beneficiary VASP via the applicable protocol. The Travel Rule transaction ends up in the 'Archived' tab.

Out Of Band, Inbound Transactions

You might receive a Travel Rule message out of band. For example, you got an email or took a phone call that relayed Travel Rule information.

To keep all Travel Rule information in the same place, you can retroactively add this information to the system.

Export archived

Export all archived transactions, add out of band transaction

Go to the 'Archive' tab on the 'Transactions' screen. You will see an 'Archive New Transaction' button in the top right corner. Enter the data in the shown pop and save.

Exports

When your AML auditor or a government official requests data from the system, you can export either a single transaction or the entire database.

Go to the inbox or archived tab to export a single transaction and expand a transaction. On the bottom left corner of the expansion, you will see an 'export' button. Clicking that button will export the transaction as an Excel/CSV file.

Similarly, you can export the entire database of archived transactions. First, go to the archived tab. Then, a button in the top right corner reads 'Export transactions'. This will export all transactions as an Excel/CSV file.

Private Wallets

The tab 'Private Wallets' shows all the address ownership proofs your VASP has received throught the Address Ownership Proof Protocol.

AOPP

The list of submitted address ownership proofs

The only column worth mentioning is the 'Signed Proof' column. This column shows the message the user saw on his wallet that he signed. Your VASP generates it.

You can export all the proofs with the 'Export Proofs' button.

Additionally you can add an Address Proof by hand. Click on the 'Add Address Proof' button and fill out the fields in the pop up shown.

Manually add address ownership proof

Manually add AOPP Proof

This feature allows you to keep all relevant information in one place, just like adding an out of band received Travel Rule transaction.

Transactions

Our Transactions tab consists of three subtabs:

Inbox

Transactions Inbox Tab

Transactions Inbox Tab


Waiting

Transactions Waiting Tab

Transactions Waiting Tab


Archived

Transactions Archived Tab

Transactions Archived Tab


Transactions Inbox

Our Transactions Inbox panel is divided into three distinct elements.

Transactions Overview
Detailed Transaction Overview
Send Transaction Notification

Transactions Inbox Overview

Our transactions inbox overview lets you easily navigate through all received transactions.

Transactions Inbox Table

Transactions Inbox Overview Table


Let's now do a breakdown of the tables row.

Transactions Inbox Table

Transactions Inbox Overview Table Row


As you can see this row contains the following fields:

Plus Details Icon
Type
Status
Beneficiary
Originator
Originator VASP
Virtual Asset
Amount
Date Created
Actions

Plus Details Icon

Transaction Details

Clicking on the plus icon shows the user detailed information about that particular transaction. Clicking it again hides the details.

To view what the detailed information looks like, check: Detailed Transaction Overview

Type

Can be either Sent or Received. Hovering over the icon will reveal a tooltip with the type of transaction.

Transaction Type Received

When the Type has the Received value, represented by the box logo above, it means a counterparty VASP has sent you, the compliance officer, a transaction. This transaction is then waiting for you to Approve it or Decline it.

Transaction Type Sent

When the Type has the Sent value, it means you have sent a transaction to a counterparty VASP through the Send Transactions Notification button, and the counterparty VASP has then approved or declined the transaction. This is then waiting for the last step, which is for you to Confirm or Cancel the transaction.

For a better understanding of the whole workflow, check: Advanced Workflow.

Status

Can be either Pending or Approved.

Transaction Status Pending

Once a transaction has been sent by a counterparty VASP to you, your inbox will go into a Pending status until you Approve or Decline the transaction. The status of Pending is of Type Received when it is in the inbox.

Transaction Status Apprvoed

If, on the other hand, you have sent a transaction to a counterparty VASP through the Send Transactions Notification button and the counterparty VASP has clicked Approve, the Status of Approved will appear in your inbox for that particular transaction. The Status of Approved is of Type Sent when it's in the inbox.

Beneficiary

The name and surname, as registered in a legal identity document of one of the account ower at your VASP. This account ower is the Beneficiary, thus the receiver of the amount stated in the transaction.

Originator

The name and surname, as registered in a legal identity document of one of the account owner at the counterparty VASP. This account ower is the Originator, thus the sender of the amount stated in the transaction.

Originator VASP

The name of the registered counterparty VASP that the Originator user has sent the amount from stated in the transaction.

Virtual Asset

Transaction Virtual Asset

Shows the symbol, name and currency code of the Virtual Asset Sent or Received in the transaction. Clicking on the name will redirect you to coinmarketcap where you can see live information about the asset of the transaction.

Amount

The amount of a particular Virtual Asset sent from the Originator to the Beneficiary. Yes, we do support the entire 18 decimals for Ethereum.

Date Created

The time and date the transaction was Sent or Received. The format shown is day/month/year and hour/minutes/seconds. The time will be shown in the time zone configured in your browser and can be seen by hovering over the date of the transaction. This will reveal a tooltip with the timezone: continent/city format.

Actions

Transaction Actions

You will find a “tick” and “cross” sign under the actions column. If you move the cursor over the icons you will see “approve” and “cancel” on a tooltip box.

If you click approve a modal will pop up requesting the user to introduce the transaction ID of the transaction in hand. This can usually be found on your wallet when doing a transaction. It can also be found in a blockchain explorer or through a command line tool for more technical users. Once the transaction has been confirmed by clicking the “confirm” button the row will disappear from the inbox and move to the archived table.

If the user presses the cancel option, a different modal will appear, allowing the user to optionally add a comment to why he has decided to cancel the transaction. Once the transaction “cancel transaction” button has been clicked the row will disappear from the inbox and move to the archived table.

Integration Guides

You, as a VASP, can integrate with several APIs.

There are APIs for:

  1. Sending, receiving and querying Travel Rule messages
  2. Showing AOPP URIs to your users
  3. Generating Travel Addresses

Ultimately all of these APIs are optional to use.

Travel Rule messages

We have built the GUI of 21 Travel Rule™ on top of a GraphQL API. A VASP can use the same API to do custom integrations.

There are many things a VASP can do with this API, for example:

  1. Automatically instruct our software to send a Travel Rule message.
  2. Set rules to accept or reject incoming Travel Rule messages automatically.
  3. Create monthly reports based on the exported data.
  4. Listen for incoming Travel Rule messages and alert an employee.

AOPP

If you opted in to use AOPP, you need to involve your users. The AOPP API allows you to do so. We encourage you to integrate the AOPP API directly in your frontend. All it takes is opening a single websocket per user session to our endpoint.

Travel Address

TRP 2.0 introduced the concept of the Travel Address. With the Travel Address, you provide a better user experience to your end-users. We, therefore, recommend that you use it.

For this to work, 21 Travel Rule™ needs to call an endpoint on your side. In essence, we translate a Travel Address with a virtual asset address.

GraphQL

We build our GUI on top of a GraphQL API. If you want to integrate 21 Travel Rule™ with your own IT infrastructure, you can use the same GraphQL API.

The API is self-documenting. The documentation linked is running on a server of ours with fake data, and, as a consequence, you are unable to use that API for testing or production. You run our software on your IT infrastructure. Find the 'Docs' link in the upper right corner to view additional documentation.

Authentication

The GraphQL API is authenticated with a secure cookie.

Creating a user

Creating a user happens with an HTTP POST request to the /users/login path. The request must include a Content-Type: application/json header. The JSON payload looks like this:

{
  "username": "some-username",
  "password": "some-password"
}

This request returns a cookie that is used to authenticate all other requests.

Authenticating requests

Obtaining the cookie from the username/password combination happens in the same fashion as when creating a user. Just repeat the request used for creating a user. In order to authenticate GraphQL requests, include the returned cookie in the request. For example:

curl -H 'content-type: application/json' -b 'user=QgMwwh9+Q52CCuS0a6HAVr5BPSBY9nkgpRv5xU1DPYW1uhB1g2bd;' -d '{"query": "{trpd { vasps { name }}}"}' 'https://testing.21analytics.xyz/graphql'

Query

As the name suggests and as is the rule with GraphQL, use the operation type 'query' to return objects from the backend. There are several objects available.

autod

The autod query groups all the different protocols. With it, you can query the backend for transactions, and the list of returned transactions contain a chronologically ordered list of transactions.

This is the query we recommend using when getting transactions from 21 Travel Rule™.

txDetails

This query returns a page of transactions independent of the protocol. For example, our 'Transactions' tab in our GUI uses this object exclusively. The status argument can be INBOX, WAITING or ARCHIVE.

Example

To return the amount from the TRP and email transactions in the inbox, run this:

{
  autod {
    txDetails(status: INBOX, limit:10 ,offset: 0) {
      ... on TrpdTx {
        trpd {
          amount
        }
      }
      ... on EmailTx {
        email {
          amount
        }
      }
    }
  }
}

txDetailsCount

This query returns the number of transactions with a particular status. Having this number is useful for paging.

Example
{
  autod {
    txDetailsCount(status: INBOX)
  }
}

txsAsCsv

As the name suggests, this returns the transactions formatted as a CSV string. Please note that the returned data is not a CSV file but a GraphQL object where a field contains the CSV data.

Example
{
  autod {
    txsAsCsv(status: INBOX)
  }
}

trpd

This object contains all the TRP (Travel Rule Protocol) related data. All this data is already included in the autod query, and we suggest using that query instead.

txDetails

The txDetails object is the most important endpoint. It exposes the TRP transaction sent and received.

Example

The object has many fields, one of which is the asset field that contains a three-letter currency symbol. Find out what assets passed through the system via TRP by running this query:

{
  trpd {
    txDetails {
      asset
    }
  }
}

The txDetails object can be paginated with an offset and limit argument and filtered with the filter argument. For acceptable values for the filter argument, see the auto-generated API documentation.

vasps

The vasps object describes the registered counter-party, TRP enabled, VASPs. Only when a beneficiary LEI of an incoming TRP message matches the LEI of one of the registered VASPs does the txDetails object return the TRP message. This prevents spam.

Example

The following query returns the name and the optional public key for each configured counter-party VASP:

{
  trpd {
    vasps {
      name
      pubkey
    }
  }
}

See the live documentation for the entire list of queryable objects and fields.

configuration

As the name suggests, this object returns the current TRP configuration of your VASP.

Example

One of the configurable attributes is the LEI. Query the current value with:

{
  trpd {
    configuration {
      lei
    }
  }
}

lookupLnurl

Use this query to translate a Travel Address into a virtual asset, an associated address and the beneficiary VASP. The query will make an HTTP request to the resolved beneficiary VASP. As such, using this query only works if the resolved beneficiary VASP is also registered.

Example

With the Travel Address obtained from the originating user you can do:

{
  trpd {
    lookupLnurl(lnurl: "LNURL1DP68GURN8GHJ7AR9WD6XJMN89CERZCTWV9K8JARFVDEJUCMG9AEHWCTS9ASHXERXTPNJZL") {
      asset
      address
      vasp {
        name
      }
    }
  }
}

Note that the vasp object has more than just a name field.

aopd

Querying AOPP related objects happens here.

proofs

When an end-user uses AOPP to create a verified withdrawal address, it creates a proof. Once a proof is submitted, it is verified. Listing verified proofs happen with this object. Pagination is supported.

Example

Get the first ten proofs with:

{
  aopd {
    proofs(offset: 0, limit: 10) {
      address
      id
    }
  }
}

configuration

AOPD allows you to configure the callback URL the user's wallet should call with the generated proof and address. Query that URL with this object.

{
  aopd {
    configuration {
      callbackUrl
    }
  }
}

proofsCount

A supporting query that returns the number of verified proofs.

Example
{
  aopd {
    proofsCount
  }
}

proofAsCsv

As the name suggests, this returns the proofs formatted as a CSV string. Please note that the returned data is not a CSV file but a JSON document in which one field contains the CSV data.

Example
{
  aopd {
    proofsAsCsv
  }
}

email

Email is the fallback mechanism for staying compliant with the FATF Travel Rule.

configuration

Calling this query returns the email configuration of 21 Travel Rule™.

Example
{
  email {
    configuration {
      smtpUsername
      smtpPassword
    }
  }
}

vasps

As with the trpd vasps query, this returns a list of registered VASPs to whom you can email.

Example

In essence, the same as the previously mentioned trpd vasps query.

{
  email {
    vasps {
      name
    }
  }
}

version

The version object returns the current version of the software.

Example

Possibly the simplest query ever:

{
  version
}

Mutation

As the name suggests and as is the rule with GraphQL, the operation type 'mutation' can be used to affect changes in the backend. Notice that the object type mutation is explicit in the examples. GraphQL allows omitting the query type but not the mutation type. There would be no way to distinguish them otherwise. There are several objects available.

trpd

Everything related to the Travel Rule Protocol.

registerTx

Performing this mutation registers a TRP transaction in the backend.

Note that either addressLine OR street and number are required.

Also, note that the assets Bitcoin, Grin and Ethereum have their own decimal accuracy of 8, 9 and 18 decimal places, respectively. All others default to 8.

Example

This example registers a transaction with an amount of 1 Tether.

mutation {
  trpd {
    registerTx(txDetails: {
      asset: USDT 
      amount: "100000000"
      originator: {
        name: {
          firstName: "some name"
          lastName: "some last name"
        }
        address: {
          postalCode: "1234"
          town: "town"
          country: "ch"
          street: "Dorpstraat"
          number: "1"
        }, 
        walletAddress: "123"
        customerIdentification: "123"
      }
      beneficiary: {
        name: {
          firstName: "some other name"
          lastName: "some last name"
        }, 
        walletAddress: "123"
      }
      beneficiaryVaspLei: "OHRD00TW7IQEDT2YCY76"
    })
  }
}

registerOrUpdateVasp

Before transactions are turned by the txDetails query, a VASP needs to be registered.

Example
mutation {
  trpd {
    registerOrUpdateVasp(vasp: {
        url: "https://demo.21analytics.ch",
        name: "Harm's Emporium",
        lei: "RILFO74KP1CM8P6PCT96",
        autoApproved: false
    }) {
      lei
    }
  }
}

distrustVasp

This removes a VASP. Transactions received from this VASP are no longer shown, but they remain in the database.

Example
mutation {
  trpd {
    distrustVasp(lei: "RILFO74KP1CM8P6PCT96")
  }
}

updateConfiguration

In order to receive TRP messages your VASP needs to be configured. That is done with this query. Creating and updating are the same things. All arguments need to be passed always.

The lnurlBaseUrl is the external facing host to be used when exchanging messages in the advanced flow.

Example
mutation {
  trpd {
    updateConfiguration(vasp: {
      name: "Harm's outfit",
      lei: "RILFO74KP1CM8P6PCT96",
      lnurlBaseUrl: "https://some.com"
    }) {
      name
    }
  }
}

obtainLnurl

When a user requests to send funds to a VASP which supports the advanced flow of TRP, a LNURL or Travel Address needs to be generated. The resulting LNURL is given to the beneficiary user, which in turn gives it to the originating user.

Example
mutation {
  trpd {
    obtainLnurl(user: {
      firstName: "Harm",
      lastName: "Aarts",
      asset: BTC,
      walletAddress: "abc"
    })
  }
}

aopd

Changing AOPP related server state happens under this object type.

updateConfiguration

The AOPP functionality of 21 Travel Rule™ knows only one configuration parameter. This parameter is called callbackUrl and governs where a user wallet should POST the proof when completing an AOPP request.

Example
mutation {
  aopd {
    updateConfiguration(config: {callbackUrl: "https://some.com"})
  }
}

registerProof

Proofs obtained via wallets that do not support AOPP can be stored with this query. For example, the end-user has manually submitted the signature for a signing challenge.

Example
mutation {
  aopd {
    registerProof(proof: {
      message: "hello", 
      asset: BTC, 
      address: "bc1qnshsvhrfl28g03k0vxdez6vua56r0c72xy9e93", 
      signature: "H1oYVmDaWxZBPEk2ou4myn1SRC20ycBUPPD5fLS+SmQ1e04Bi1J9mIJ5fNhe3khDhJRUX2fU+VHGKlJdAjYIvBU="
    })
  }
}

email

Email is the fallback mechanism. When all else fails, email makes sure you are still compliant.

updateConfiguration

Here you can configure your VASPS email server to send outgoing emails.

Example
mutation {
  email {
    updateConfiguration(config:{
      smtpHost: "some host"
      smtpPort: 25
      smtpUsername: "some smtp user"
      smtpPassword: "some smtp password"
      smtpTlsSec: TLS
      fromEmailAddress: "from@my-vasp.com"
      notificationEmailAddress: "notification@my-vasp.com"
    }) {
      smtpHost      
    }
  }
}

registerVasp

Sending emails can only be done to previously registered VASPs. Use this query to register email VASPs.

Example
mutation {
  email {
    registerVasp(vasp: {
      name: "some name"
      email: "compliance@other-vasp.com"
    })
  }
}

distrustVasp

When an email VASP is no longer required, use this query to remove it from the list. Previously sent transactions are retained.

Example
mutation {
  email {
    distrustVasp(id:"some id")
  }
}

registerTx

The same consideration hold here as with the registerTx mutation under the trpd object. Again, either addressLine OR street and number are required.

Example
mutation {
  email {
    registerTx(txDetails: {
      asset:BTC
      amount:"1230"
      txId:"68cf8b54b8af3e81c7d6186d20aada6a8896de4c9329b79427950b84334262ff"
      originator:{
        name: {
          firstName:"some first name"
          lastName:"some last name"
        }
        address:{
          country:"ch"
          town:"zug"
          postalCode: "1111"
          addressLine:"Dorpstraat 1"
        }
        walletAddress:"123"
        customerIdentification:"some id"
      }
      beneficiary: {
        name: {
          firstName:"some first name"
          lastName:"some last name"
        }
        walletAddress:"123"
      }
      beneficiaryVaspId:"123"
    })
  }
}

offline

Sometimes Travel Rule information is received out of band. 21 Travel Rule™ allows entering that information in the system nonetheless. Doing so makes sure that travel rule compliance data is held at a single source of truth.

registerTx

The mutation is very similar to the other registerTx mutations. The notable difference is that there's a field called originatorVasp. Because this Travel Rule data was received out of band, it is required to specify which VASP originated the request. To this end, the name field is required and the lei field is optional.

Example
mutation {
  offline {
    registerTx(txDetails: {
      asset:BTC
      amount:"1230"
      txId:"68cf8b54b8af3e81c7d6186d20aada6a8896de4c9329b79427950b84334262ff"
      originator:{
        name: {
          firstName:"some first name"
          lastName:"some last name"
        }
        address:{
          country:"ch"
          town:"zug"
          postalCode: "1111"
          addressLine:"Dorpstraat 1"
        }
        walletAddress:"123"
        customerIdentification:"some id"
      }
      beneficiary: {
        name: {
          firstName:"some first name"
          lastName:"some last name"
        }
        walletAddress:"123"
      }
      originatorVasp: {
        name: "some name"
      }
    })
  }
}

AOPP integration guide

21 Travel Rule makes it easy for VASPs to integrate AOPP into their stack. This document outlines what is required from the VASP to successfully deploy AOPP support. The subsystem in 21 Travel Rule handling AOPP support is called aopd.

We define several entities:

  • User: this is the end-user of the VASP
  • VASP: this is the software with which the User interacts
  • 21 Travel Rule: the software licenced from 21 Analytics
  • Private Wallet: the User's private cryptocurrency wallet
  • Custody: the subsystem of the VASP responsible for onchain transactions, the interaction with this system can be automated or manual

Core flow

The VASP and 21 Travel Rule communicate via a single websocket. This socket is initiated by the VASP and a single message is send to 21 Travel Rule. 21 Travel Rule will put two messages on the socket. One immediate after the first message from the VASP and the other once the Private Wallet submitted the signed proof. One socket connection is used for one proof. Once the second message is received by the VASP the connection is closed by 21 Travel Rule. When a new proof is required a new connection should be opened.

The following sequence diagram depicts the core flow: Sequence diagram basic

1. Request AOPP URI

The VASP requests an AOPP URI from the 21 Travel Rule. It does this by opening a websocket. The socket is served at the root of the location where 21 Travel Rule is hosted. For example when the software is hosted on 21travel.<your-domain>.com the websocket URL is wss://21travel.<your-domain>.com/. After opening the socket a JSON document is sent on that socket with the format:

{
    "msg": "some message choosen by the VASP to be displayed in the user's wallet",
    "asset": "an asset identifier. Possible values are 'btc' or 'eth'",
    "format": "the required address format. Possible values are 'p2pkh', 'p2wpkh', 'p2sh', 'p2tr' or 'any'"
}

Depending on the Custody solution a value for format needs to be choosen. If possible any is preferred.

2. AOPP URI

A response is sent immediately by 21 Travel Rule. That looks like this:

aopp:?v=0&msg=vasp-chosen-msg&asset=btc&format=p2xxx&callback=https://vasp.com/xxxx

3. Address

Once a signed proof is received from the Private Wallet a single string containing an address is send to the VASP by 21 Travel Rule. An example message is "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa".

At this point the websocket is closed.

Full example scenario

In this scenario the User wants to withdraw a specific amount. On the withdraw page of the VASP the User enters the amount he wants to withdraw and after he submits the form is prompted to provide a verified address via the AOPP protocol.

A new address is requested from the user each time but the User interaction is kept at a minimum.

The withdraw flow is displayed in the following diagram, with the core flow as the dashed lines: Sequence diagram AOPP

1. Withdrawal request

The User is logged in on the VASP's website and initiates a withdrawal request. Critically the request contains an amount the User wants to withdraw and the amount is available to the User to actually withdraw. At this point, the VASP knows of the withdrawal request of that particular user and the amount.

2. Request AOPP URI

The VASP opens a websocket and requests an AOPP URI from the 21 Travel Rule. The msg parameter is chosen by the VASP and is suggested to contain a meaningful message as it is displayed to the User in his Private Wallet. A good example would be "I confirm that this Bitcoin (BTC) address is controlled by Jennifer Cox, Poststrasse 22, Zug, Switzerland. Unique Identifier: 2238833c7ff51f53". It contains a meaningful statement on what the User is about to do and an identifier for internal correlation.

3. AOPP URI

A response is sent immediately by 21 Travel Rule. That looks something like this:

aopp:?v=0&msg=I+confirm+that+this+Bitcoin+%28BTC%29+address+is+controlled+by+Jennifer+Cox%2C+Poststrasse+22%2C+Zug%2C+Switzerland.+Unique+Identifier%3A+2238833c7ff51f53&asset=btc&format=any&callback=https%3A%2F%2Fdemo.21analytics.ch%2Fproofs%2F1dade3e8-cd22-4a05-b96e-49bf3e2d5e34

4. Display AOPP URI

The VASP now displays the AOPP URI to the User. Either by encoding it in a QR code or by making it into a HTML link. The User is then expected to scan the QR with their supported wallet or click the link which will open a supported wallet.

5. Signed proof

The User then simply clicks or taps the pop up in his Private Wallet upon which the Private Wallet generates an address and sends a signed proof to 21 Travel Rule.

6. Address

Upon reception of the signed proof from the Users Private Wallet a new message is sent on the existing websocket connection to the VASP containing a single address.

7. Initiate withdrawal process

The VASP now has in hand the amount and the verified address and can proceed with the actual withdrawal process.

TRP

Authoritative source

Travel Rule Protocol

VersionDateDescription
1.0.019th August 2020Initial Public Version
1.0.110th December 2020Bug fix
1.1.08th February 2021Clarifications, bug fixes and consistency enhancements
2.0.010th May 2021Addition of LNURL enhanced flow
2.1.031st Oct 2021LNURL advanced flow with inquiry tag
3.0.015th Feb 2022mTLS for client and server authentication
deprecate enhanced workflow

Executive Summary

Inherently, involvement within the virtual asset ecosystem presents higher risks related to money laundering and terrorist financing than those found in the traditional world of fiat currency. Primarily this is due to the lack of transparency of the public keys, which are not associated with any identity.

Whilst in the traditional world, there is proven infrastructure with processes and tools designed to mitigate AML/CFT risks, given the evolution of the virtual asset ecosystem, there is now a need to find comparable solutions not only to address industry concerns but incoming global regulations. Key operators such as custodians and market participants must consider the following issues or gaps:

  1. The current inability to identify originators and/or beneficiaries of transactions.
  2. Cumbersome processes and governance to be put in place in order to confirm to whom a defined public key belongs.

There are varying approaches across the industry to define a common set of standards and to create a mechanism for the exchange of relevant information including identity and associated address details all using a common infrastructure, protocol and data set.

Introduction

The Travel Rule working group was primarily established to address a specific recommendation issued by the Financial Action Task Force ("FATF"). Members of the working group include leading industry participants from around the world who meet regularly to discuss and agree on finding a common solution to meet the regulatory standards. The information within this paper seeks to highlight a solution, referred herein as the Travel Rule Protocol (TRP).

The TRP vision is to define a set of standards which will allow Virtual Asset Service Providers (VASPs - as defined as in footnote1 below) to safely share with other trusted participants the identity of an originator and beneficiary linked to a specific virtual asset transfer. This set of standards aims to meet the requirements set by the FATF Recommendation 16 (R16), commonly known as the Travel Rule defined as below:

R16 -- Countries should ensure that originating VASPs obtain and hold required and accurate originator information and required beneficiary information on virtual asset transfers, submit the above information to the beneficiary VASP or financial institution (if any) immediately and securely, and make it available on request to appropriate authorities. Countries should ensure that beneficiary VASPs obtain and hold required originator information and required and accurate beneficiary information on virtual asset transfers, and make it available on request to appropriate authorities. Other requirements of R16 (including monitoring of the availability of information, and taking freezing action and prohibiting transactions with designated persons and entities) apply on the same basis as set out in R16. The same obligations apply to financial institutions when sending or receiving virtual asset transfers on behalf of a customer.

It is intended that adoption of the TRP is royalty and license free. However, any fees and licenses related to implementation, service of software providers, etc. will be the responsibility of the implementing VASP, including but not limited to document the legal and contractual arrangement ensuring that any exchange of information is done in compliance with applicable laws, e.g. GDPR.

The TRP has been incrementally developed by a self-selecting group of leading industry participants from around the world, collaborating openly and sharing to develop and evolve the solution. There is no single central body or organization behind the protocol, only collaboration.

The TRP working group has a regular weekly minuted meeting with a set agenda. There are other forums including chat rooms for more frequent collaboration as well as email for more infrequent formal announcements. The working group welcomes all contributions, ideas and input to the design and implementation of the protocol. Industry participants who wish to help developing the protocol, or adopt the protocol are encouraged to register at https://travelruleprotocol.org/

Any comments, corrections or suggestions on this version of the protocol specification can also be captured via the feedback form at https://travelruleprotocol.org

Goals

The goal of the TRP is to develop a first-generation, minimal, workable, and pragmatic API specification for compliance with the FATF Travel Rule in a timely manner, that also minimizes fragmentation and maximizes interoperability.

First Generation

Industry participants acknowledge that mainstream adoption of virtual assets as an asset class will take time, and encouragingly there is evidence that its evolution is progressing with increasing focus by regulators and central banks imposing regulatory requirements. The TRP aims to initiate one of the first steps on this journey by launching a first-generation solution for compliance with the FATF Travel Rule.

Minimal, Workable, Pragmatic API

The TRP has been developed with sufficient flexibility allowing straightforward adoption for a wide range of industry participants. By focusing closely on FATF R16 and limiting the scope of the solution, the TRP aims to create a simple API specification for required information exchange that will seamlessly integrate with existing business IT frameworks.

Minimize Fragmentation & Maximize Interoperation

There are a number of potentially more comprehensive Travel Rule solutions in development across the industry participants. By focusing closely on FATF R16 and limiting the scope of the solution, the TRP aims to create a simple API specification for the information exchange that is easy to implement and integrate with existing business solutions, as well as having an easy path to interoperate with other emerging industry Travel Rule solutions as they mature. Any investment made should be transferable to future industry solutions.

Design Philosophy

The underlying design principles of the Travel Rule Protocol:

  1. Compliance with FATF R16
  2. Data privacy: Bilateral exchange for minimal data leakage
  3. Iterative and incremental with early MVP launch
  4. Open for industry collaboration and adoption
  5. Aligned with emerging industry standards such as IVMS101
  6. Any investment made should be transferable to future industry solutions
  7. Each member controls the data that they share

Indication of Requirement Levels

This document follows rfc2119 to indicate requirement levels.

Unspecified Behavior

TRP leaves certain aspects of the protocol unspecified. It is a deliberate decision that enables to accommodate as many use cases as possible. This is a necessity due to the nature of travel rule which spans various jurisdictions and needs to be applicable to a diverse variety of VASPs.

We use the following sanity check when judging about unspecified behavior:

When two VASPs implement a certain aspect differently but still are able to interoperate then we MUST omit detailing that aspect in the specification.

If you have found cases that are unspecified but violate the above sanity check then this is considered a bug and we'd kindly ask you to open an issue.

Example

The layout for callback URLs is unspecified because when two VASPs implement them differently they are still able to interoperate. Leaving them unspecified gives VASPs the freedom to encode any data into the URL that the VASP might find useful. It also allows us to extend the protocol without breaking backwards compatibility with respect to callback URLs.

Advanced Workflow

In the advanced workflow, the originator VASP receives a special payment address, called Travel Address, instead of a normal blockchain address from the end user. This Travel Address is a bech32 encoded URL which contains a unique URL. Bech32 encoded URLs are standardized as LNURL2. Once the LNURL is decoded by the originator VASP a request is made to that URL to inquire for permission to execute an transaction.

This workflow hence allows to:

  • decline transfers
  • conduct KYC checks
  • easily share transaction ids

which solves many practical problems for compliance teams.

Minimal Core Protocol with Extensions

The first part of the goals of the TRP:

"first generation, minimal, workable, and pragmatic API specification for compliance with the FATF Travel Rule in a timely manner"

is centered around having a simple minimal protocol that is easy to implement and to comply with.

The second part of the goals is to:

"minimize fragmentation and maximize interoperation"

In order to meet both of these goals the TRP is separated into two parts:

  1. A minimal core protocol
  2. Extensions

The minimal core protocol should contain mandatory elements that cover the bare minimum FATF R16 requirements. This meets the first part of the goals.

However, it is acknowledged that one size does not fit all, taking into account differing jurisdictional and compliance requirements on what information is to be exchanged. To help accommodate the second goal an extension mechanism is proposed.

The minimum core protocol will comply with the mandatory elements of R16, all other requirements will be added using the extension mechanism.

Security

TRP uses TLS 1.3 to secure all protocol traffic. This is a well-understood and widely-adopted solution for communication security.

Client Authentication

Clients are authenticated using a X.509 certificate. Those are most commonly used to authenticate a server when using TLS.

It is referred to as mutual TLS (mTLS) when both, client and server, are authenticated using X.509 certificates.

In TRP, a server MUST implement the /identity endpoint. A server's client certificate MAY be obtained by a GET request to the /identity endpoint. It is returned in a JSON document to allow for future extension. The expected response looks as following:

HTTP 200

{
    "x509": [x509 certificate in PEM format]
}

The PEM format is of type string. Further details can be obtained here.

TRP servers are required to reject connections from clients that cannot authenticate themselves using mTLS. The only exception is the /identity endpoint.

A VASP is free to choose any CA provider. In TRP, mTLS is used purely for establishing a secure communication channel. There is no additional semantics attached to the X509 certificates.

Because mTLS is established and well-understood it can be used without custom implementation out-of-the-box with nginx.

Technical implementation - General

Core Protocol

Headers

Each request must have the following header fields:

FieldDescriptionExample value
api-versionVersion of the API3.0.0
api-extensionsComma separated list of extensions to the base the TRP protocol to be used when processing this request.1request-signing,beneficiary-details-response
request-identifierUnique UUID V4 value for this particular API call (different for all calls)2351f3f1-bcff-4a06-a07a-8de94220a9b0

1: Use of extensions is optional and must be agreed on between parties. A server should be able to inform a client it is unable or unwilling to support the extensions as defined in the client request's api-extensions header. Two HTTP response codes are applicable, the 422 - Unprocessable Entity and the 501 - Unimplemented codes. Either one of them is applicable in different extension cases. See "Extensions" section of the document for more details.

Each response must include the headers api-version and request-identifier. The former holds the version of the API the responder is using, the latter echoes the request-identifier back to the sender.

Sample Header

api-version: 3.0.0
api-extensions: request-signing,beneficiary-details-response
request-identifier: 2351f3f1-bcff-4a06-a07a-8de94220a9b0

Extensions

Entirely optional, zero or more extensions can be supported. Users of the TRP protocol should agree on a bilateral basis on which extensions they will use.

Scope

Extensions can add any and all HTTP headers or parameters.

The body of the requests can be added to the designated location.

Caveats

Conflicts between extensions

With the number of extensions growing the risk of conflicts or incompatibilities between extensions grows exponentially. New extension authors should take special care to investigate and address any conflicts.

Promotions from extension to core

Extensions might prove a comfortable testing ground for new functionality. When an extension is universally used it could be promoted to the core protocol. The promotion mechanism is still to be decided.

Fallback

Implementations should cater to as many extensions as possible. Some form of auto-discovery and if practical fallback should be catered for.

Core protocol additions

In order to support extensions some extension-specific handling is required in the core protocol beyond what is necessary for R16 compliance. These are described in the following section.

API-Extensions header

This header contains a comma separated list of predefined extension names. It must be omitted when no extensions are enabled.

Extension not supported response code

A server should be able to inform a client that it is unable or unwilling to support the extensions as defined in the client request's api-extensions header. Two HTTP response codes are applicable, the 422 - Unprocessable Entity and the 501 - Not Implemented. Either one of them is applicable in different extension cases.

Request body

Extensions have a reserved top level key in JSON payloads called extensions. Each extension then has a key below that with a specific payload. For example:

{
  "IVMS101": ...
  "extensions": {
    "offchainData": {
      "refId": ...,
    }
  }
}

When no extensions are used in the request, i.e. the api-extensions header is absent, the key extensions must be omitted.

Technical implementation - Advanced Workflow

Motivation

The advanced workflow solves the following issues:

  1. enables to decline transactions a priori by using a transfer inquiry. This reduces the workload significantly for the compliance team.

  2. enables to negotiate a payment address after the KYC checks have been completed. This eliminates most unwanted payments.

  3. enables to share the transaction id after the blockchain payment has been executed by using the transfer confirmation. This eliminates the need for signed but unbroadcasted transactions that require low-level tinkering and are risky because they can be executed by simply broadcasting them. The transaction id is useful for the beneficiary to check if he has received the payment and trigger a human interaction in case it wasn't received.

Overview

actor SENDING_USER #lightblue
entity VASP_ORIGINATOR #blue

actor RECEIVING_USER #lightgreen
entity VASP_BENEFICIARY #purple


group FATF Compliant TRP\nRECEIVING_USER wants to receive BTC from SENDING_USER\nHappy Path

RECEIVING_USER -> VASP_BENEFICIARY: I would like to receive funds
VASP_BENEFICIARY -> RECEIVING_USER: Generates LNURL LNURL1DP68GURN8GHJ7... which encodes\nhttps://beneficiary.com/api?q=3fc3645b439c&tag=travelRuleInquiry
RECEIVING_USER -> SENDING_USER: Gives LNURL (LNURL1DP68GURN8GHJ7...)\n+beneficiary details
SENDING_USER -> VASP_ORIGINATOR: Sends P1=(LNURL,\nBeneficiary Details)
VASP_ORIGINATOR -> VASP_ORIGINATOR: KYC Checks Beneficiary Details

autonumber
VASP_ORIGINATOR -> VASP_BENEFICIARY: Send IVMS101 payload to decoded LNURL
note right: {\n  "IVMS101": ...,\n  "asset": {\n    "slip0044": [asset identifier as per SLIP-0044]\n  },\n  "amount": [amount in lowest denomination],\n  "callback": "https://originator.com/inquiryResolution?q=4585839457"\n}

autonumber stop
VASP_BENEFICIARY -> VASP_BENEFICIARY: KYC Checks on Sender Details
autonumber resume

VASP_BENEFICIARY -> VASP_ORIGINATOR: POST HTTP 200 to https://originator.com/inquiryResolution?q=4585839457
note left: {\n  "approved": [payment address],\n  "callback": "https://beneficiary.com/transferConfirmation?q=3454366424"\n}

autonumber stop
VASP_ORIGINATOR -> VASP_ORIGINATOR: KYC on [payment address]
VASP_ORIGINATOR -> VASP_ORIGINATOR: Creates an on-chain transfer\nresulting in a tx hash
autonumber resume

VASP_ORIGINATOR -> VASP_BENEFICIARY: POST HTTP 200 to https://beneficiary.com/transferConfirmation?q=3454366424
note right: {"txid": [some asset-specific tx identifier]}
end

Detailed advanced workflow

An example advanced LNURL looks like this:

LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXFN5V9NN6ARJV9MX2MZJW4KX2JTWW96KJUNEVXPY3S

It encodes:

https://beneficiary.com/implementation/defined/path&tag=travelRuleInquiry

The tag query parameter is mandatory. According to the LNURL spec the tag query parameter indicates special meaning. In this case the presence of travelRuleInquiry means that a POST request should be made containing the following body.

{
    "asset": {
        "slip0044": [asset identifier as per SLIP-0044],
    },
    "amount": [amount in lowest denomination of asset],
    "callback": "https://originator.com/implementation/defined/path/for/inquiryResolution",
    "IVMS101": {
        "originator": {
            "originatorPersons": [
                {
                    "naturalPerson": {
                        "name": {
                            "nameIdentifier": [
                                {
                                    "primaryIdentifier": "Post",
                                    "secondaryIdentifier": "Johnny",
                                    "nameIdentifierType": "LEGL"
                                }
                            ]
                        },
                        "geographicAddress": [
                            {
                                "addressType": "GEOG",
                                "streetName": "Potential Street",
                                "buildingNumber": "123",
                                "buildingName": "Cheese Hut",
                                "postCode": "91361",
                                "townName": "Thousand Oaks",
                                "countrySubDivision": "California",
                                "country": "US"
                            }
                        ],
                        "customerIdentification": "1002390"
                    }
                }
            ]
        },
        "beneficiary": {
            "beneficiaryPersons": [
                {
                    "naturalPerson": {
                        "name": {
                            "nameIdentifier": [
                                {
                                    "primaryIdentifier": "MachuPichu",
                                    "secondaryIdentifier": "Freddie",
                                    "nameIdentifierType": "LEGL"
                                }
                            ]
                        }
                    }
                }
            ]
        },
        "originatingVASP": {
            "originatingVASP": {
                "legalPerson": {
                    "name": {
                        "nameIdentifier": [
                            {
                                "legalPersonName": "VASP A",
                                "legalPersonNameIdentifierType": "LEGL"
                            }
                        ]
                    },
                    "nationalIdentification": {
                        "nationalIdentifier": "506700T7Z685VUOZL877",
                        "nationalIdentifierType": "LEIX"
                    }
                }
            }
        }
    }
}

SLIP-0044 is available here. The JSON is structured such that it can be easily extended to support other asset identifier standards in the future.

Response data

HTTP OK 200

{
    "version": "3.0.0"
}

Transfer Inquiry Resolution

The callback URL from the transfer inquiry is used to send the following POST request

{
    "approved": {
        "address": "some payment address",
        "callback": "https://beneficiary.com/implementation/defined/path/for/transferConfirmation"
    }
}

or

{
    "rejected": "human readable comment" [or null]
}

Response data

HTTP 204 No Content

Transfer Confirmation

The callback URL from the transfer inquiry resolution is used to send the following POST request

{
    "txid": [some asset-specific tx identifier]
}

or

{
    "canceled": "human readable comment" [or null]
}

The txid should only be communicated if the transaction has been confirmed on-chain.

Response data

HTTP 204 No Content

General error handling

Invalid/Expired URLs

An LNURL can expire (e.g. customer closes account) or be invalid (e.g. someone crafts a URL with arbitrary parameters). Also a callback URL can expire (e.g. already called or timed out) or be invalid by construction. A backend must reply in such a case with an HTTP status code 404 NOT FOUND.

Error on server side

If an error is triggered on the server side (e.g. database unreachable) then a backend must reply with HTTP status code 500 INTERNAL SERVER ERROR.

Compliance with FATF Recommendation 16

The key part of the FATF VASP Recommendations is:

A. As described in INR.15, paragraph 7(b), all of the requirements set forth in Recommendation 16 apply to VASPs or other obliged entities that engage in VA transfers, including the obligations to obtain, hold, and transmit required originator and beneficiary information in order to identify and report suspicious transactions, monitor the availability of information, take freezing actions, and prohibit transactions with designated persons and entities.

By exchanging originator and beneficiary information between VASPs, the TRP facilitates compliance with this requirement. Information must be stored by each VASPs themselves. No central or distributed database is proposed.

A. Further, countries should ensure that beneficiary institutions (whether a VASP or other obliged entity) obtain and hold required (not necessarily accurate) originator information and required and accurate beneficiary information, as set forth in INR. 16. The required information includes the:

  1. originator's name (i.e., the sending customer); The TRP facilitates compliance with this requirement.
  2. originator's account number where such an account is used to process the transaction (eg: the VA wallet); The TRP facilitates compliance with this requirement. The account is the public address.
  3. originator's physical (geographical) address, or national identity number, or customer identification number (i.e., not a transaction number) that uniquely identifies the originator to the ordering institution, or date and place of birth; The TRP facilitates compliance with this requirement. The TRP working group has agreed to use LEI or GEOG from IVMS101 in first phase.
  4. beneficiary's name; and The TRP facilitates compliance with this requirement.
  5. beneficiary account number where such an account is used to process the transaction (eg: the VA wallet). It is not necessary for the information to be attached directly to the VA transfer itself. The information can be submitted either directly or indirectly, as set forth in INR. 15. The TRP facilitates compliance with this requirement. The account is the public address.

Authoritative Version

This document provides a description of the TRP and its usage. Where possible this document will be updated and new versions issued as the protocol is revised.

References

1

See definition of Virtual Asset Service Providers at https://www.fatf-gafi.org/glossary/u-z/ 2: https://github.com/fiatjaf/lnurl-rfc

End of Document

AOPP

Authoritative source

Address Ownership Proof Protocol (AOPP)

Mission Statement

The AOPP streamlines and automates address ownership proofs, which are required in interactions between private wallets and Virtual Asset Service Providers (VASPs), for example in virtual asset withdrawal.

Specification

Link on the "Address Ownership Proof" web page of a VASP:

<a href="​aopp:?v=0&msg=vasp-chosen-msg&asset=btc&format=p2xxx&callback=https://vasp.com/proofs/vasp-chosen-token​">verify address</a>

Required parameters in version 0:

  1. v: version number, always 0

  2. msg: a vasp-chosen message up to 1024 ASCII characters; it is recommended to contain a ​nonce​ value

  3. asset: a virtual asset identifier (as defined in ​SLIP-0044​)

  4. format: specifies a wallet address type the VASP expects in a callback;

    1. for "btc" asset it is:

      • p2pkh
      • p2wpkh
      • p2sh
      • p2tr
      • any
    2. for "eth" asset it is:

      • standard
  5. callback: a VASP chosen URL endpoint where the client wallet sends the response to.

When a wallet encounters an unknown version number or other unrecognised or invalid values in required parameters, it should display an error and abort signing. Wallets should ignore parameters not listed above.

On click, a desktop app shows up. The user confirms the message. The app sends a response to the server:

Response:

POST <callback URL>
Content-Type: application/json; utf-8
{
    "version": 0,
    "address": "bc1000000000000000000000000",
    "signature": "​<Bitcoin Signed Message Signature>​"
}

Response from the Server:

  1. HTTP 204: Signature is valid.
  2. HTTP 400: Bad Request.
  3. HTTP 404: The vasp-chosen-token in the callback URL doesn't exist.

Compressed Keys Only

Uncompressed keys are considered legacy and are not supported.

Bitcoin Message Signatures

For bitcoin, the signatures are constructed following the algorithm in bitcoin core. Special attention should be brought to the encoding of the recovery byte that does not indicate information about witness addresses in its encoding. This is the main difference between bitcoin core's message signatures and other, less popular ones.

Please refer to the wallet guide for further implementation hints.

Ethereum Message Signatures

For ethereum, the signatures are constructed following the same algorithm as in bitcoin core with the notable exception that the recovery byte is appended to the signature instead of prepended.

Recommendations

  1. Choose any for the address format if your VASP supports all possible address formats. Certain wallets (e.g. electrum) have a fixed address format and therefore letting them pick the address format will increase interoperability.
  2. Choose a small message to improve UX using hardware wallets (small screens). Also, URIs have an implementation-defined limit so smaller URIs will have better interoperability in general.
  3. [Add SRO approved message style here]

FAQ

  1. Why is there a format specifier?

    A format specifier allows a VASP to hint to a wallet what address format it can accept. Historically, certain VASPs have had poor support for various address formats.

  2. How to deal with p2sh addresses?

    For p2sh addresses we need to presume that it's a p2wpkh address wrapped in a p2sh.

  3. Why is the wallet choosing the address?

    Certain wallet types, such as hardware wallets, are limited in how they can handle specific workflows. To facilitate the adoption in all kinds of wallets, the protocol lets the wallet choose the address.

  4. Why is there a limit on the msg parameter?

    Some wallets have a hard limit on the size of the message, hence the 1024 character limit in AOPP.

Test vectors

Bitcoin

  • p2pkh
    • private key: L4MSJRS7EZNoinjUXJAKrtSgvA6epQDAmwgo5B2LJdVCcjEapPE1
    • address: 13LiZwTMfowMo5KsWHf5TNLmK78WSxVQCG
    • message: hello
    • signature: IFwh9XVPb8vvNUsbuuQU1Xk1jT652JD/6HN3cqnFn/MMBDziPbM8cOi83D29LRGSwMF3ZcjytD7nfNdn5dI0/50=
  • p2sh (wrapped SegWit)
    • private key: L5fETAwYWGRA1eWCHk8AgH2FX2rxLp64winwoAoRGpz1aQMp3Rai
    • address: 3JvVkfeKrrJstF66haNpFepfhxzQuBB78h
    • message: hello
    • signature: H1oYVmDaWxZBPEk2ou4myn1SRC20ycBUPPD5fLS+SmQ1e04Bi1J9mIJ5fNhe3khDhJRUX2fU+VHGKlJdAjYIvBU=
  • p2wpkh
    • private key: L5fETAwYWGRA1eWCHk8AgH2FX2rxLp64winwoAoRGpz1aQMp3Rai
    • address: bc1qnshsvhrfl28g03k0vxdez6vua56r0c72xy9e93
    • message: hello
    • signature: H1oYVmDaWxZBPEk2ou4myn1SRC20ycBUPPD5fLS+SmQ1e04Bi1J9mIJ5fNhe3khDhJRUX2fU+VHGKlJdAjYIvBU=

Ethereum

  • private key: 4142e80a872531fd1055f52ccab713d4c7f1eee28c33415558e74faeb516de2b
  • address: 0x270402aeB8f4dAc8203915fC26F0768feA61b532
  • message: hello
  • signature: vbyudz7PM/tUdVjlL0wEtCnvn3PVYv8eCqCf/aLeVj8JwAsUVuyMMwbIInXAj7EtmZIUwlem7AOH0da8ygXmQBs=

Wallet Guide

Authoritative source

Guide for adding AOPP support to a wallet

Register AOPP URI handler

The app needs to register itself as an AOPP URI handler in the OS.

Sample URI

aopp:?v=0&msg=hello&asset=btc&format=p2wpkh&callback=https://api.testing.21analytics.xyz/proofs/vasp-chosen-token

Android

https://developer.android.com/guide/topics/manifest/intent-filter-element

Linux

See this example.

Windows

See this example.

iOS

https://celsiusnotes.com/url-schemes-in-ios/

If more than one app registers to handle the same URI scheme (e.g aopp: or also bitcoin:) there is no way to tell which app will be opened by iOS (see https://stackoverflow.com/a/13130477).

macOS

Add the following to your plist file:

<key>CFBundleURLTypes</key>
<array>
	<dict>
		<key>CFBundleURLName</key>
		<string>com.example.wallet</string>
		<key>CFBundleURLSchemes</key>
		<array>
			<string>aopp</string>
		</array>
	</dict>
</array>

Docs on how to handle the URI: https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app

Handle AOPP URIs via QR-code scanning

That only makes sense for mobile wallets.

Generate new address/pubkey

Generate a new address that follows the format from the AOPP URI format query string. Only addresses from compressed pubkeys are supported.

Sign msg from query string

In the AOPP URI, the msg query string contains the message to be signed using Bitcoin's sign message algorithm, i.e. this one.

Example with bitcoinjs-message

Note the omission of a segwit specifier for all address types.

const bitcoinMessage = require('bitcoinjs-message')
bitcoinMessage.sign(message, privateKey, keyPair.compressed)

Send result

Send result to the callback obtained from the AOPP URI callback query string. It should be a POST request with the following JSON body.

{
    "version": 0,
    "address": "address generated above",
    "signature": "signature created above"
}

The signature should be base64-encoded.

Implementation Playground

An end-user-facing webapp is made available for testing here.

You can see when your address has been registered in the 21 Analytics software stack, which features an AOPP-compatible backend: https://testing.21analytics.xyz/aopp

Username: admin

Password: admin

The logs of the docker AOPP backend container running on the server (for debugging) are available here: http://testing.21analytics.xyz:8888

Reference Implementation

There is a fork of the popular desktop wallet Electrum with built-in support for AOPP available here: https://gitlab.com/aopp/electrum

Open Source Wallets with AOPP Support

  • BlueWallet
    • Pull Request: https://github.com/BlueWallet/BlueWallet/pull/2915
  • Bitbox
    • Pull Requests: https://github.com/digitalbitbox/bitbox-wallet-app/pulls?q=is%3Apr+aopp+is%3Aclosed
    • Code (excluding the URI registration): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/backend/aopp.go
  • Sparrow Wallet
    • Code: https://github.com/sparrowwallet/sparrow/commit/425e476f202273dcc4e721a3aa92defca31196f4

Security

In this section we give an overview of our security processes, designs and mitigations.

Security-first development approach

Rust

We use Rust as our backend development language. Rust is a memory-safe language that compiles to machine code and thus doesn't compromise on performance while eliminating the primary source for security-related bugs [1][2][3].

Rust has seen growing adoption in both critical open source software [4][5][6][7] and at Fortune 500 companies [8][9][10][11].

Code Review

We have strict code review policies in place that demand that every change to the source code needs to be reviewed and approved by another developer. Only then it can be merged into master. Among other aspects the reviewer is asked to focus on security. As a side-effect of our code review practices, know-how is spread across the team that shares the responsibility for the source code. This eliminates malware injection or sabotage from malicious insiders.

Continuous Integration

Our continuous integration pipeline runs tests, linter checks and builds. Only if all tasks run successfully a change can be merged into master.

Employing a security-in-depth approach, we use the cargo-audit [12] vulnerability scanner to automatically scan our entire dependency tree for versions with reported security vulnerabilities. This is also run by the continuous integration system and hence, will block offending changes from being merged into master. If issues arise on the master branch then all merge requests will we blocked until the issue has been fixed on master.

Dependency Management

We use tools that monitor our dependencies (libraries, docker images, etc.) and open merge requests once newer versions of dependencies become available. After they pass our continuous integration they are reviewed and can be confidently merged into master without introducing regressions or breaking the build. Updates that fail continuous integration are handled manually to ensure a quick update.

Having an efficient and effortless dependency management process enables us to constantly ship up-to-date software to our customers.

Security by Design

Secure Architecture

Separation of Concerns

We have designed a micro service architecture where we apply the separation of concerns [13] principle. APIs accessible from the public internet are exposed in different micro services than APIs that interface with the custodial system.

In our reference deployment [14] those logical separations are enforced with a configuration that also splits them on a network level.

Principle of least privilege

Our services are stateless and use a database for persistence. We use database roles and permissions in accordance to the principle of least privilege [15] to refuse one microservice from accessing another's data.

Mitigation

We are aware that even when a memory-safe language like Rust is used disaster can strike nonetheless. Our services are containerized to isolate them from the host system. Furthermore, we statically compile our binaries which allows us to run them in empty containers. This significantly limits the attack surface in case of a successful memory-corruption exploit. For example, an attacker could not even gain access to libc or any binary useful for escalating the exploit chain like e.g. a shell.

Resources

[1] https://www.chromium.org/Home/chromium-security/memory-safety/
[2] https://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/
[3] https://security.googleblog.com/2021/02/mitigating-memory-safety-issues-in-open.html
[4] https://daniel.haxx.se/blog/2021/02/09/curl-supports-rustls/
[5] https://security.googleblog.com/2021/04/rust-in-linux-kernel.html
[6] https://security.googleblog.com/2021/04/rust-in-android-platform.html
[7] https://www.abetterinternet.org/post/preparing-rustls-for-wider-adoption/
[8] https://learn.microsoft.com/en-us/windows/dev-environment/rust/rust-for-windows
[9] https://engineering.fb.com/2021/04/29/developer-tools/rust/
[10] https://aws.amazon.com/blogs/opensource/why-aws-loves-rust-and-how-wed-like-to-help/
[11] https://aws.amazon.com/blogs/opensource/sustainability-with-rust/
[12] https://rustsec.org/
[13] https://en.wikipedia.org/wiki/Separation_of_concerns
[14] https://docs.21analytics.ch/deployment.html
[15] https://en.wikipedia.org/wiki/Principle_of_least_privilege