How to build a crypto exchange

How to build backend for your exchange in 30 minutes.

Introduction

The crypto exchange is a web application where users can trade their crypto assets. The operator of the exchange is the one who owns the private keys to all of the user's crypto assets - we are talking about a custodial exchange leveraging a custodial wallet.

A custodial wallet is a wallet where a third party holds the private keys, not the crypto assets owner. The provider has full control over crypto assets, while users only have to permit to send or receive payments.

Every exchange must have wallets for every supported crypto assets it supports. Every user of the exchange must obtain accounts for every asset he/she is trading. The exchange operator defines the trading pairs, which can be traded by users, and usually charges a fee for every performed trade.

‌Trades are not performed on the blockchain (on-chain). That would be extremely slow and expensive. All the trades are only virtual transactions between user's accounts.

‌ There are four logical groups of actions that must be done to create an exchange:

  • The application set up - This includes prerequisites like blockchain wallets creation or creation of exchange service accounts for gathering fees.

  • Registration of new users into the application - Steps needed to be executed when new users register into the exchange.

  • User's application journey - What kind of operations users do during their journey in the application.

  • Trading - Enabling users to trade their assets.

In the next sections, we will use Bitcoin and Ethereum as blockchains to demonstrate the functionalities.

Application set up

This phase is a one-time step that must be done before the launch of the exchange. It involves creating the blockchain wallets your application will support or creating service fee accounts for the wallet provider.

Create blockchain wallets

To generate a Bitcoin wallet, you need to call a request to the Bitcoin/wallet endpoint. For Etherum, it works the same. Only the endpoint is different. The result contains two fields - mnemonic and xpub.

Request
Response
Request
curl --request GET \
--url 'https://api-eu1.tatum.io/v3/bitcoin/wallet' \
--header 'x-api-key: YOUR_API_KEY'
Response
{
"mnemonic": "zebra parent avocado margin ready heart space orchard police junior travel today bag action rough system novel large rain detail route spare add mail",
"xpub": "tpubDE8GQ9vAXpwkp37PCCRUpCoeShpC4WiCcACxh8r3nnKjfRPRqw3w58EgkfNiBy1MaRqX1oAAxwAxauEUG7vWupSh5m15znGy7vE7aE6CWzb"
}

To generate a wallet for any ERC-20 token like USDT, LINK or others, use the same call as for Ethereum. These tokens are transported on the Ethereum network leveraging the same Ethereum addresses as native Ether.

Blockchain wallets here are created using API, which is not a secure way of generating wallets. Your private keys and mnemonics should never leave your security perimeter. To correctly and securely generate a wallet, you can use Tatum CLI from the command line or use a complex Key Management System from Tatum KMS.

Generate service accounts

For every supported blockchain wallet, a service ledger account should be created. These accounts will be used to gather fees from the trading of the users. Every performed trade will be charged with the fee, and the fee will be transferred to the account.

‌Optionally, every account can belong to the concrete customer in Tatum. The customer inside Tatum is an entity containing information about your application's user, like the customer's country of residence, accounting currency, etc. The customer is created only during the new account's creation, and the only required field is external ID. For service accounts, you can group all service accounts into one service customer.

Accounting currency is part of the compliance engine built inside Tatum. It's enabled by default.

Every account should have an accounting currency set up correctly. It should be a FIAT currency of the country, where accounting is performed. E.g., the exchange will run in Germany, accounting should be in EUR, so accounting currency is EUR. More details are available in the API Reference.

Request
Response
Request
curl --location --request POST 'https://api-eu1.tatum.io/v3/ledger/account' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"currency": "BTC",
"accountingCurrency": "EUR",
"customer": {
"externalId": "SERVICE_CUSTOMER_EXTERNAL_ID"
}
}'
Response
{
"currency": "BTC",
"active": true,
"balance": {
"accountBalance": "0",
"availableBalance": "0"
},
"frozen": false,
"accountingCurrency": "EUR",
"customerId": "5fb7bdf6e96d9ab593e191a6"
"id": "5fb7bdf6e96d9ab593e191a5"
}

Registration of the new user

After the configuration is done and the exchange is live, users are registering into the ecosystem.

Create accounts for the user

When a new user signs up for the application, ledger accounts must be created for them. Every user should have an account of every supported blockchain asset in the exchange. Every account should be created with the external ID of the customer. Thanks to that, it will be possible to list all accounts for the specific customer. An account should also have set up accounting currency correctly.

The customer's external ID should be a unique identifier of the user in your application, e.g., your ID or the hash.

Every account in the private ledger must have a defined currency. Currency cannot be changed in the future. During the creation of the account, xpub from the blockchain wallet must be entered. This is the first connection between the blockchain and the ledger.

You will use the xpubs from the wallets generated during the application set up.

Request
Response
Request
curl --location --request POST 'https://api-eu1.tatum.io/v3/ledger/account' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"currency": "BTC",
"xpub": "tpubDE8GQ9vAXpwkp37PCCRUpCoeShpC4WiCcACxh8r3nnKjfRPRqw3w58EgkfNiBy1MaRqX1oAAxwAxauEUG7vWupSh5m15znGy7vE7aE6CWzb",
"accountingCurrency": "EUR",
"customer": {
"externalId": "SERVICE_CUSTOMER_EXTERNAL_ID"
}
}'
Response
{
"currency": "BTC",
"active": true,
"balance": {
"accountBalance": "0",
"availableBalance": "0"
},
"frozen": false,
"accountingCurrency": "EUR",
"xpub": "tpubDE8GQ9vAXpwkp37PCCRUpCoeShpC4WiCcACxh8r3nnKjfRPRqw3w58EgkfNiBy1MaRqX1oAAxwAxauEUG7vWupSh5m15znGy7vE7aE6CWzb",
"customerId": "5fb7bdf6e96d9ab593e191a6"
"id": "5fb7bdf6e96d9ab593e191a5"
}

Generate blockchain deposit address for the account

Once the account is created, it is not yet synchronized with the blockchain. There is no blockchain address connected to it, only a blockchain wallet, from which addresses will be chosen. To connect a specific address, you need to generate the account's address using the off-chain method Generate address for the account.

Request
Response
Request
curl --location --request POST 'https://api-eu1.tatum.io/v3/offchain/account/5fb7bdf6e96d9ab593e191a5/address' \
--header 'x-api-key: YOUR_API_KEY'
Response
{
"xpub": "tpubDE8GQ9vAXpwkp37PCCRUpCoeShpC4WiCcACxh8r3nnKjfRPRqw3w58EgkfNiBy1MaRqX1oAAxwAxauEUG7vWupSh5m15znGy7vE7aE6CWzb",
"derivationKey": 1,
"address": "mgSXLa5sJHvBpYTKZ62aW9z2YWQNTJ59Zm",
"currency": "BTC"
}

The result is the blockchain address, which was connected to the ledger account. Any incoming blockchain transaction to this address will be automatically synchronized to the private ledger.

Enable notification on incoming blockchain transactions

It is possible to enable webhook notifications on every incoming transaction to the account. This notification is fired as an HTTP POST request with a JSON body with the fields like the amount of the transaction, currency, and account of the incoming transaction. Users should see somewhere in their wallet page that there are incoming pending transactions - their crypto deposits.

Request
Response
Request
curl --location --request POST 'https://api-eu1.tatum.io/v3/ledger/account' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"attr": {
"id": "5fb7bdf6e96d9ab593e191a5",
"url": "https://webhook.site/"
},
"type": "ACCOUNT_INCOMING_BLOCKCHAIN_TRANSACTION"
}'
Response
{
"id": "5fef7ab888eef2e9e4927913"
}

The result is the ID of the subscription for later deletion.

User's journey

The user has successfully signed up, accounts and deposit addresses for all supported blockchains are created. Let's take a look, what he should see in the application itself.

List of user's accounts with balances

When the user signs in to the application, a list of their accounts should be visible. We can obtain all accounts for one user using his customer ID.

The account's balance is available in the accounts list by default and does not have to be queried separately. There are two types of balances:

  • The account balance is the total balance of the account without any pending deposits or other trade blockages.

  • The available balance is the balance that can be used on a trade or other types of transactions.

Request
Response
Request
curl --location --request GET 'https://api-eu1.tatum.io/v3/ledger/account/customer/5fb7bdf6e96d9ab593e191a6?pageSize=50' \
--header 'x-api-key: YOUR_API_KEY'
Response
[
{
"currency": "BTC",
"active": true,
"balance": {
"accountBalance": "0.001",
"availableBalance": "0.001"
},
"frozen": false,
"accountingCurrency": "EUR",
"xpub": "tpubDE8GQ9vAXpwkp37PCCRUpCoeShpC4WiCcACxh8r3nnKjfRPRqw3w58EgkfNiBy1MaRqX1oAAxwAxauEUG7vWupSh5m15znGy7vE7aE6CWzb",
"customerId": "5fb7bdf6e96d9ab593e191a6"
"id": "5fb7bdf6e96d9ab593e191a5"
}
]

List of last transactions on any account

Usually, also last transactions that happened on any of the accounts are presented as well.

Request
Response
Request
curl --location --request POST 'https://api-eu1.tatum.io/v3/ledger/transaction/customer?pageSize=50' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": "5fb7bdf6e96d9ab593e191a6"
}'
Response
[
{
"amount": "0.001",
"operationType": "DEPOSIT",
"currency": "BTC",
"transactionType": "CREDIT_DEPOSIT",
"accountId": "5fb7bdf6e96d9ab593e191a5",
"anonymous": false,
"reference": "c81a23dd-e162-4e0b-b0ff-e470c64f7b88",
"txId": "cd63e729ecc513bc22e8632b69a433126d5621c5f11047f34a0cbe144ce9aaac",
"address": "n22crsZTASULKtLqg3XzD1NwV1HnfrQpcd",
"marketValue": {
"currency": "EUR",
"source": "CoinGecko",
"sourceDate": 1606164328453,
"amount": "15.49693999999999884022"
},
"created": 1606164532855
}
]

The user can see the detail of the account and transactions connected only to this account.

Obtain deposit address for account

Usually, it is good to display blockchain addresses connected to this account to send a blockchain transaction to the exchange.

Request
Response
Request
curl --location --request GET 'https://api-eu1.tatum.io/v3/offchain/account/5fb7bdf6e96d9ab593e191a5/address' \
--header 'x-api-key: YOUR_API_KEY'
Response
[
{
"xpub": "tpubDE8GQ9vAXpwkp37PCCRUpCoeShpC4WiCcACxh8r3nnKjfRPRqw3w58EgkfNiBy1MaRqX1oAAxwAxauEUG7vWupSh5m15znGy7vE7aE6CWzb",
"derivationKey": 1,
"address": "mgSXLa5sJHvBpYTKZ62aW9z2YWQNTJ59Zm",
"currency": "BTC"
}
]

Withdraw funds from exchange to the blockchain

For every blockchain, there is a specific API call for performing withdrawal. We will cover Bitcoin in this section, but it works similarly in others.

Request
Response
Request
curl --location --request POST 'https://api-eu1.tatum.io/v3/offchain/bitcoin/transfer' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"senderAccountId": "5fbaca3001421166273b3779",
"address": "mpTwPdF8up9kidgcAStriUPwRdnE9MRAg7",
"amount": "0.00195",
"fee": "0.00005",
"mnemonic": "behave season capable ridge repair creek seat rescue potato divide fox expose wrestle asthma luggage rack afford pistol ridge modify direct picnic magic cannon",
"xpub": "tpubDF1sYuDKCJr6mGietaVzqGmF2dqdKVBa1DtLJGBX8HXhtHZPv5UBz3WNWU22tiVAYSjqfvfFxMnDs3vM11iQrKej6dq33UCevhiPW9EQAS2"
}'
Response
{
"txId": "97bc1c3c23b179cba837e4060c0d07aa399f7ac7d34d91a7405cb5f801b93c8a",
"id": "5fbc208c99a159b4e9120c30",
"completed": true
}

You can see that the required parameters are the ledger account's identifier, information about the blockchain wallet, recipient blockchain address, amount to be sent, and blockchain fee to be paid.

Trading

Last but not least, is the ability to perform trades. Bear in mind that this section describes exchanges like Binance or Coinbase, where the exchange provider is responsible for every trading pair's liquidity that the exchange support.

You can imagine liquidity of the pair as how much open buy / sell trades are present on the Order book and how much volume is traded during the day. Higher the liquidity, more precise chart and the price of the asset is more accurate.

Open new trade

Every user can open an unlimited number of trades. Trade can be BUY or SELL and is connected to the concrete trading pair. Trading pairs are created automatically with the first opened trade.

The trading pair consists of 2 assets. Let's discuss the BTC/ETH pair. The first asset is Bitcoin, and the second is Ethereum. When you open new BUY trade with the pair BTC/ETH, you want to buy Bitcoin for your Ethereum.

Every trade must have a price and an amount of the asset you want to trade. By default, in Tatum, every trade is a LIMIT trade, and you have to wait until the price hits your target.

You want to buy 1 Bitcoin for 40 Ethereum. You have to open BUY BTC/ETH trade with the price set to 40 and the amount set to 1. Your trade is open until there will be another opposite trade opened. This opposite trade should be SELL BTC/ETH with the price set to 40 or below and the amount set to 1 or more.

MARKET trades can be executed by setting the price above or below the highest BUY or lowest SELL.

When you open a trade, there are two accounts you must enter:

  • ledger account with the currency of the first asset in the trading pair

  • ledger account with the currency of the second asset in the trading pair

The traded amount will be blocked and debited from one of these accounts based on the trade type, and the other account will be credited with the traded asset when the trade will be fulfilled.

Let's buy 1 BTC for 40 ETH in BTC/ETH pair. Account 1 is BTC, and account 2 is ETH. 40 ETH will be blocked from the ETH account and then transferred to the ETH account of the opposite SELL trade. To the BTC account, 1 BTC will be credited from the BTC account of the opposite SELL trade.

Every trade must be filled and closed at some time. It is possible to fill only the part of the trade, not all of it. There might be trade open for selling 1 BTC for 40 ETH, but the opposite trade was executed only to buy 0.5 BTC. Your actual trade will be partially filled for 0.5 BTC and stays open until the rest 0.5 BTC won't be closed.

There is a ledger transaction performed for every partial fill of the trade, and assets are transferred to the ledger accounts associated with the trade. Also, blockage is decreased accordingly.

Enough of the theory, let's open the BUY trade.

Request
Response
Request
curl --location --request POST 'https://api-eu1.tatum.io/v3/trade' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "BUY",
"price": "40",
"amount": "1",
"pair": "BTC/ETH",
"currency1AccountId": "5f914e372e47312bc56d8d3d",
"currency2AccountId": "5f914e0a2e47312bc56d8d3b"
}'
Response
{
"id": "5e68c66581f2ee32bc354087"
}

The response is the ID of this open trade. When you list the blockages on the ETH account, you will see that there is a blockage of 40 ETH. Additional information is present in the blockage, such as trade ID as a description.

Request
Response
Request
curl --location --request GET 'https://api-eu1.tatum.io/v3/ledger/account/block/5f914e0a2e47312bc56d8d3b?pageSize=10' \
--header 'x-api-key: YOUR_API_KEY'
Response
[
{
"amount": "40",
"type": "TRADE",
"description": "5ff0d13a5f4813e187a2a498",
"accountId": "5e68c66581f2ee32bc354087",
"id": "5ff0d13a5f4813e187a2a499"
}
]

No transaction has been performed yet, only the blockage.

List open trades

When you have opened trades, which are not yet filled, you can list them using the List active trades endpoint. You can list BUY and SELL opened trades using two different endpoints. Keep in mind that using these endpoints, you list either all opened trades across all pairs or only trades for a specific account by providing the account's id as a query parameter.

Request
Response
Request
curl --location --request GET 'https://api-eu1.tatum.io/v3/trade/buy?pageSize=10' \
--header 'x-api-key: YOUR_API_KEY'
Response
[
{
"created": 1609617722713,
"amount": "1",
"price": "40",
"fill": "0",
"type": "BUY",
"pair": "BTC/ETH",
"currency1AccountId": "5f914e372e47312bc56d8d3d",
"currency2AccountId": "5f914e0a2e47312bc56d8d3b"
"fee": null,
"feeAccountId": null,
"id": "5e68c66581f2ee32bc354087"
}
]

You can see that there is no fill of the trade, which means that no opposite SELL trade has matched this trade's price. This trade was not opened with fee and fee account ID.

The fee can be executed by providing fee and feeAccountId property when opening trade. It is always the first currency of the trading pair and is set up in percent.

List historical trades

When the trade is closed, there are two ledger transactions. The first one is between accounts of the trading pair's first currency. The second one is between accounts of the second currency in the trading pair. In BTC/ETH example, there is ledger to ledger transactions of BTC and ledger to ledger transaction of ETH. Blockages on both trades are deleted, and trade is not active anymore and is moved to the historical trades.

To see the list of historical trades, you can call the List all historical trades endpoint.

Request
Response
Request
curl --location --request GET 'https://api-eu1.tatum.io/v3/trade/history?pageSize=10' \
--header 'x-api-key: YOUR_API_KEY'
Response
[
{
"created": 1609617722713,
"amount": "1",
"price": "40",
"fill": "1",
"type": "BUY",
"pair": "BTC/ETH",
"currency1AccountId": "5f914e372e47312bc56d8d3d",
"currency2AccountId": "5f914e0a2e47312bc56d8d3b"
"fee": null,
"feeAccountId": null,
"id": "5e68c66581f2ee32bc354087"
}
]

You can see that the only difference is the fill property, which is the same as the trade amount.

‌ That's it. There are many more things to enhance and many more features to have, but this could be a good start for you and your exchange.