Sodot Exchange API Vault - Tutorial
Welcome to the Exchange API Vault integration tutorial!
In this quick guide, you'll get hands-on experience with the core components of the Exchange API Vault.
This simple walkthrough is designed to help you understand how the Vault works before diving into a full-scale integration with your trading engine.
By the end, you'll have a clear understanding of how to securely import, trade, and enforce policies for your exchange API keys using the Vault.
In addition to this tutorial, we provide a demo trading platform that showcases an integration with a full trading platform. We will reference this demo throughout the tutorial, so you can see how the concepts we discuss here are applied as part of a larger architecture.
Tutorial Components Overview

Vertex Cluster (MPC Signers)
The Exchange API Vault is built on top of the Vertex cluster, which is a self-hosted distributed key management system that leverages both MPC and secure enclaves (a.k.a. TEEs) technology to securely store and manage sensitive keys.
A Vertex cluster is deployed in a three-node cluster configuration, where each node is a separate instance of a Vertex service. The nodes communicate with each other using a secure relay server, which allows them to share keys and perform signing operations in a distributed manner.
For this tutorial, we will be using a demo environment that includes three Vertex servers hosted by Sodot. These are hosted at the following URLs:
- Vertex 0:
https://demo-exchange-vault-0.sodot.dev - Vertex 1:
https://demo-exchange-vault-1.sodot.dev - Vertex 2:
https://demo-exchange-vault-2.sodot.dev
These servers are set up to allow you to test the Exchange API Vault without needing to set up your own Vertex cluster. You'll be able to import your API keys, trade, and enforce policies using these demo servers.
The demo environment is intended for testing purposes only.
It is not suitable for production use, and you should not use it to store sensitive keys or perform real trades.
Key Sharing SDK (For HMAC API Keys)
The Exchange API Vault provides a web SDK that allows you to shard the exchange API keys right at the end user's browser. This SDK is designed to be integrated seamlessly into any existing customer or internal web portals that are used to import keys into your trading system. These shards are encrypted directly for the three Vertex servers, alleviating the worry that all parts of your system architecture would have access to the full exchange API key during the import process.
Example Policy
The Exchange API Vault allows you to define programmable policies that will be enforced before signing any API payload.
These policies can be used to restrict the usage of the API keys based on any custom business logic.
For example, you can define a policy that restricts the usage of the API key to only allow trades on certain pairs, or to only allow withdrawals below a certain amount.
This is particularly useful for high-value API keys, such as withdrawal keys, where you want to ensure that any illicit transfers or trades are stopped before they ever reach the exchange.
In this tutorial, we will enforce a pre-deployed policy server that is already configured on the demo Vertex servers.
This policy denies trades between BTC and USDT above 0.5 BTC for Binance testnet API requests.
This policy can be easily modified to suit your specific needs, and you can define multiple policies for different API keys or use cases.
The source code for this policy can be found in the reference implementation trading platform code here.
To attach this policy to your API keys, use its name which is limit-half-btc-orders in the demo Vertex servers.
Prerequisites
Before you start, make sure you have the following:
- A Binance testnet account with an HMAC-SHA256 API key and secret. These can be issued here.
- API keys for the three demo Vertex servers. These can be obtained by contacting the Sodot team.
- An NPM token for the web SDK. This can be obtained by contacting the Sodot team.
Getting Started
At a high level there are three main steps for integration—each secures a different stage of an exchange API key lifecycle:
-
Key Import / Key Generation
- HMAC API Keys: Shard and encrypt them in the browser and securely import them into the MPC signers (Vertex servers).
- Ed25519 API Keys: Securely generate keys with MPC directly within the Vertex cluster and provide the public key to the exchange.
-
Trade / Transfer - Trade using the sharded API key without ever exposing the key to the trading machine and without added latency.
-
Enforce Policies - Set and enforce custom policies for use with highly-sensitive API keys, such as withdrawal keys, ensuring illicit transfers/trades are stopped before they ever reach the exchange.
Code Example
Here is a minimal code example that implements the steps that we will walk through for the PoC integration.
We will reference this code throughout the tutorial, and we recommend you clone it and run it afterwards.
It is separated into 2 main files:
hmac-rest-api-trading.ts- Demonstrates how to shard an HMAC API key in the browser, import it into the Vertex servers, and use it to trade over Binance REST API.ed25519-websocket-trading.ts- Demonstrates how to generate a distributed Ed25519 API key and use it to trade over Binance WebSocket API.
Importing / Generating API Keys
The first step is to import or generate the API keys that will be used for trading. This can be done in two ways, depending on the type of API key you are using:
- HMAC API Keys: These are the most common type of API keys used by exchanges. They are used to sign requests to the exchange API and are typically generated by the exchange and provided to you via the exchange web UI.
- Ed25519 API Keys: These API keys use public-key cryptography to sign requests. They are more secure than HMAC keys as you generate the private key and upload only the public key to the exchange.
For this tutorial, we will demonstrate both methods using the demo Vertex servers.
Secure Key Import (HMAC API Keys)
The first step is to import the HMAC API key into the Vertex servers. This is done by sharding and encrypting the key in the browser and then importing the shards into the Vertex servers.
Sharding the API Key in the Browser
For most trading use cases, there are two scenarios: either your customers are providing you with their API keys or members of your team are in charge of importing exchange API keys to your trading engine.
For both of these cases, we provide a web SDK that allows you to shard the exchange API keys directly in the end user's browser. These shards are encrypted directly for the three Vertex servers, alleviating the worry that any parts of your system architecture would have access to the full exchange API key.
To use the web SDK for sharding the key, see this code snippet:
import { splitHmacSha256KeyIntoEncryptedShares } from "@sodot/sodot-hmac-key-sharing";
// ...
// ...
async function getVertexPublicKey(vertex_url: string) {
const response = await fetch(`${vertex_url}/cluster/persistent-keygen-id`);
if (!response.ok) {
throw new Error(
`Failed to fetch public key from vertex ${vertex_url}: ${response.statusText}\n${await response.text()}`
);
}
const data = await response.json();
if (!data.keygen_id) {
throw new Error(`No keygen_id found in response from vertex ${vertex_url}`);
}
return data.keygen_id;
}
// ...
// ...
const apiKeyBytes = Uint8Array.from(binanceApiSecret, (char) =>
char.charCodeAt(0)
);
const verticesPubkeys = await Promise.all(
VERTICES.map((v) => getVertexPublicKey(v.url))
);
const shares = await splitHmacSha256KeyIntoEncryptedShares(
apiKeyBytes,
verticesPubkeys[0],
verticesPubkeys[1],
verticesPubkeys[2]
);
Import the Sharded Key into the Vertex Servers
Now that our keys are sharded, we can load each shard of the key into each Vertex.
This can be done as shown below:
await Promise.all(
VERTICES.map(async (vertex, index) => {
const response = await fetch(
`${vertex.url}/cluster/hmac-sha256/import-secret-share`,
{
headers: {
Authorization: vertex.apiKey,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
cluster_name: clusterName,
hmac_secret_share: shares[index],
key_name: keyName,
}),
}
);
if (!response.ok) {
throw new Error(
`Failed to import secret share to Vertex ${index}: ${
response.statusText
}\n${await response.text()}`
);
}
console.log(`Secret share imported to Vertex ${index}`);
})
);
Key Generation (Ed25519 API Keys)
When you are able to use Ed25519 API keys, it is recommended to generate them directly within the Vertex cluster. This allows you to securely generate the key pair in a distributed manner, eliminating the risk of exposing the private key.
How Key Generation Works
When you generate a key, the Vertex cluster uses MPC to create a threshold, so each node holds only a share and the full private key is never reconstructed or exposed.
You receive a key ID, which you can use to perform actions with the Vertex, such as signing or retrieving the public key.
You can also create the key with a human-readable name that you can use to reference it later instead of using the generated ID.
The following code generates a new Ed25519 key:
async function generateDistributedKey(keyName: string): Promise<string> {
// We need to create a room for the distributed key generation
const roomUuid = await createRoom();
// Perform key generation on all vertices
// This will create a distributed Ed25519 key across the cluster
const keygenReqBody = {
room_uuid: roomUuid,
key_name: keyName,
num_parties: BINANCE_CONFIG.NUM_PARTIES, // Number of Vertex servers in the cluster - 3
threshold: BINANCE_CONFIG.THRESHOLD, // Security threshold for extracting the key
cluster_name: process.env.CLUSTER_NAME,
};
await Promise.all(
VERTICES.map(async (vertex) => {
await sendVertexRequest(
`${vertex.url}/cluster/ed25519/keygen`,
vertex.apiKey,
keygenReqBody
);
})
);
// ..
}
The public key can then be retrieved:
// Get the public key from the first Vertex
// This is the public key that will be registered with Binance
const derivePubkeyResponse = await sendVertexRequest(
`${VERTICES[0].url}/ed25519/derive-pubkey`,
VERTICES[0].apiKey,
{
key_name: keyName,
derivation_path: [], // We don't need to derive the keys
}
);
if (!derivePubkeyResponse || !derivePubkeyResponse.pubkey) {
throw new Error(
`Failed to get the public key of "${keyName}": ${JSON.stringify(
derivePubkeyResponse
)}`
);
}
// Convert the public key to PEM format for Binance key registration
return await pubkeyToPEM(derivePubkeyResponse.pubkey);
Then go to the Binance Testnet - Register API Key page and paste the public key you got from the Vertex. In the example code, the app prompts you to do this and waits for you to provide the API key that Binance generates for you.
Trade / Transfer
Once we have the key shards loaded into the three Vertex servers, we can now use them for trading without exposing the API key to our trading machine. This can be done with REST, WebSocket, or FIX APIs.
For HFT, we recommend using either the WebSocket or FIX APIs. These allow you to achieve zero added latency when using the Exchange API Vault, since the only payload that requires signing using the API key is the first "logon" message. All other trades in the same session can be sent without signing them. In this tutorial we will show how to use the WebSocket API with an Ed25519 key, but it can be done similarly with HMAC keys.
We will show an example of using the REST API (using an HMAC key) and WebSocket (using an Ed25519 key) to trade on Binance testnet. This consists of three steps:
- Create a relay room that will be used to perform MPC signing.
- Sign the payload using the Vertex servers.
- Send the signed payload to the exchange API.
Create a Relay Room
The Vertex servers use a relay server to communicate with each other and perform MPC operations.
A relay room is a session-specific UUID that allows the relay to direct traffic between the different participants of the MPC operation.
So the first step is to create a relay room that will be used for signing the payload.
Every Vertex server exposes a /create-room endpoint that can be used to create a relay room.
We'll simply use the first Vertex server to create the room, but you can use any of the three servers.
async function createRoom(): Promise<string> {
const apiKey = VERTICES[0]?.apiKey;
if (!apiKey) {
throw new Error("API key is not defined for the first Vertex.");
}
const response = await fetch(`${VERTICES[0]?.url}/create-room`, {
method: "POST",
headers: {
Authorization: apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ room_size: 3 }),
});
if (!response.ok) {
throw new Error(`Failed to create room: ${response.statusText}`);
}
const roomData = (await response.json()) as { room_uuid: string };
return roomData.room_uuid;
}
Sign the Payload
The next step is to sign the payload using the Vertex servers.
The signing process is done using the /hmac-sha256/sign or /ed25519/sign endpoint, which takes the payload and the room UUID as input and returns the signed payload.
We will send the payload to all three Vertex servers and make sure that they all return the same signature.
- HMAC Signing
- Ed25519 Signing
// We will use a utility method to sign the payload
async function signPayload(clusterKeyName: string, payload: string) {
const roomUuid = await createRoom();
const hexPayload = Buffer.from(payload, "utf-8").toString("hex");
const body = JSON.stringify({
message: hexPayload,
room_uuid: roomUuid,
key_name: clusterKeyName,
extra_data: "",
});
const signatures = await Promise.all(
VERTICES.map(async (vertex) => {
const response = await fetch(`${vertex.url}/hmac-sha256/sign`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${vertex.apiKey}`,
},
body,
});
if (!response.ok) {
throw new Error(
`Failed to sign payload on Vertex ${vertex.url}: ${
response.statusText
}\n${await response.text()}`
);
}
return response.json() as Promise<{ signature: string }>;
})
);
if (signatures.some((sig) => sig.signature !== signatures[0]?.signature)) {
throw new Error("Signatures do not match across vertices");
}
return signatures[0]?.signature;
}
async function signWithEd25519Key(
keyName: string,
message: string
): Promise<string> {
const roomUuid = await createRoom();
const signRequestBody = {
room_uuid: roomUuid,
key_name: keyName,
msg: Buffer.from(message, "utf-8").toString("hex"),
};
const signatures = await Promise.all(
VERTICES.map(async (vertex) => {
const response = await sendVertexRequest(
`${vertex.url}/ed25519/sign`,
vertex.apiKey,
signRequestBody
);
if (!response || !response.signature) {
throw new Error(
`Failed to sign message with Ed25519 key "${keyName}" on Vertex ${vertex.url}: No signature in response`
);
}
return response.signature;
})
);
// Check that all signatures are the same
if (signatures.some((sig) => sig !== signatures[0])) {
throw new Error("Signatures do not match across vertices");
}
return signatures[0];
}
Trade Using REST API
Now that we know how to sign payloads with the Vertex cluster, we'll use the method defined above to sign the payload and send it to the exchange API This approach allows you to sign and send orders just as you would with a typical HMAC signing library, but with the added security of distributed key management.
async function buySomeBtc(binanceApiKey: string, clusterKeyName: string) {
const params = {
symbol: "BTCUSDT",
side: "BUY",
type: "MARKET",
quantity: "0.0001",
timestamp: Date.now().toString(),
};
const queryString = new URLSearchParams(params).toString();
const signature = await signPayload(clusterKeyName, queryString);
// Send the signed payload to Binance API
const response = await fetch(
`https://testnet.binance.vision/api/v3/order?${queryString}&signature=${signature}`,
{
method: "POST",
headers: {
"X-MBX-APIKEY": binanceApiKey,
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(
`Failed to place order: ${response.statusText}\n${await response.text()}`
);
}
const orderData = await response.json();
console.log("Order placed successfully:", orderData);
}
Trade Using WebSocket API
The WebSocket API allows for real-time trading and is particularly useful for HFT (high-frequency trading). The WebSocket API requires an initial authentication step using a signed session logon message before you can start trading without additional signatures for subsequent actions in the session. This means that you can achieve zero added latency for HFT after the initial logon.
To begin, let's implement a function that given a WebSocket connection, the Binance API key, and the cluster key name, will authenticate the session by sending a signed logon message.
async function authenticateSession(
ws: WebSocket,
binanceApiKey: string,
clusterKeyName: string
) {
const timestamp = Date.now();
const payload = `apiKey=${binanceApiKey}×tamp=${timestamp}`;
const signature = await signWithEd25519Key(clusterKeyName, payload);
const requestId = randomUUID();
console.log(
`Authenticating session with request ID: ${requestId}, timestamp: ${timestamp}, signature: ${signature}`
);
await ws.send(
JSON.stringify({
id: requestId,
method: "session.logon",
params: {
apiKey: binanceApiKey,
timestamp: timestamp,
signature: Buffer.from(signature, "hex").toString("base64"),
},
})
);
}
Then, you can use this function to authenticate the session when you open a WebSocket connection to Binance's testnet:
const ws = new WebSocket(BINANCE_CONFIG.TESTNET_WS_URL);
ws.onopen = async () => {
console.log("Connected to Binance WebSocket API. Next is authentication...");
await authenticateSession(ws, binanceApiKey, clusterKeyName);
// Sleep for three seconds to ensure the session is authenticated
// In a real application, just subscribe to the response of the logon request
await setTimeout(() => {}, 3000);
console.log(
"Session authenticated. Now we can execute the trading strategy..."
);
// ...
};
Once authenticated, you can place orders directly over the WebSocket connection.
No additional signatures are required for subsequent trades in the same session, allowing for true zero added latency.
async function executeTradingStrategy(ws: WebSocket) {
// For the sake of this example, we will just place three market orders
// In a real application, you would implement your HFT trading strategy here
await Promise.all(
[1, 2, 3].map(async () => {
const requestId = randomUUID();
await ws.send(
JSON.stringify({
id: requestId,
method: "order.place",
params: {
symbol: "BTCUSDT",
side: "BUY",
type: "MARKET",
quantity: 0.001,
timestamp: Date.now(),
},
})
);
console.log(`Placed market order with request ID: ${requestId}`);
})
);
}
Enforce Policies
For sensitive API keys, such as withdrawal keys, you can define and enforce custom policies governing their use.
If you use a local Vertex cluster and want to define your own policies, you can do so by following the Policies guide.
For this tutorial, we'll use the demo Vertex servers, which already have a policy server deployed and configured with a simple policy that denies trades between BTC and USDT above 0.5 BTC. The code for this policy server is available in the demo trading platform repository.
Attaching the Policy Server to API Keys
Below we show how to attach the policy to the API key we imported earlier:
async function enforcePolicy(keyName: string, policyName: string) {
// It's important to attach the policy for all three vertices
// to enjoy the full benefits of the distributed system
await Promise.all(
VERTICES.map(async (vertex) => {
const response = await fetch(
`${vertex.url}/admin/policies/attach-policy-to-key`,
{
method: "POST",
headers: {
Authorization: vertex.apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
key_name: keyName,
policy_name: policyName,
}),
}
);
if (!response.ok) {
throw new Error(
`Failed to attach policy to key using Vertex ${vertex.url}: ${
response.statusText
}\n${await response.text()}`
);
}
})
);
console.log(`Policy ${policyName} attached to key ${keyName} successfully.`);
}
Signing with the Policy Attached
Now that we have the policy attached to the API key, we can check if the policy is enforced by signing a payload that violates the policy.
For example, we can edit the buySomeBtc method to buy 1 BTC instead of 0.0001 BTC.
The error we will get is:
Error: Failed to sign payload on Vertex https://demo-exchange-vault-0.sodot.dev: Bad Request
{
"err_type":"policy_validation_failed",
"err_msg":"Rule deny-above-half-btc-rule failed with error:
`Rule rejected this operation due to: Policy doesn't allow to move more than 0.5 BTC in a single order`"
}
This error indicates that the policy was enforced and the payload was rejected because it violated the policy.
Revert the change to buy 0.0001 BTC, and you should be able to place the order successfully.
What Next?
You should now plan your integration.
Instead of integrating all three aspects of the Sodot Exchange API Vault at once, you can implement a staggered migration flow, allowing you to gain security benefits from day one with minimal integration.
Then, gradually complete the migration to enjoy the full security benefits that the Exchange API Vault can provide.
The staggered approach typically includes three steps:
- Integrate the Exchange API Vault only as a distributed key management system.
- At this stage, importing the keys and using them for trading will be done similarly to a KMS or a secrets manager, except that the keys are sharded-at-rest.
- For this stage, you can use the HMAC Key Sharing SDK to shard the API keys in the browser and import them into the Vertex servers. Essentially only following the steps in the Secure Key Import section above.
- Then, you can use the export API to export the exchange API key from the Vertex servers and use it for trading as you would normally do.
- Integrate the new signing flow so that trading machines are never exposed to the exchange API keys. By following the steps in the Trade/Transfer section above.
- Determine the policies you want to enforce on the API keys and set up a policy server to enforce them. By following the steps in the Enforce Policies section above.