This is the third article about the quantitative practice of DEX exchanges. In this article, we will introduce the user guide of the Vertex protocol.
Preface
In the framework of traditional decentralized exchanges (DEX), quantitative traders often need to compromise: either accept the high slippage and low execution efficiency of the automated market maker (AMM) model, or fall into the technical dilemma of cross-chain liquidity fragmentation and single derivative function. The emergence of Vertex Protocol is redefining the on-chain territory of quantitative strategies with a "decentralized + institutional-level" fusion experiment - there is no "choose one or the other" problem here, only the ultimate balance of speed, depth and freedom.
As the first DEX to integrate a unified multi-chain liquidity pool, hybrid order book (CLOB) and embedded currency market, Vertex takes "centralized experience, decentralized soul" as its core and opens up a unique track for quantitative traders:
A new definition of speed and fluidity
As blockchain technology continues to evolve, the boundaries between traditional centralized exchanges (CEX) and decentralized exchanges (DEX) are blurring gradually. As the hub of the Vertex platform, Vertex Edge not only reshapes transaction speed and liquidity, but also combines excellent order matching technology and self-custody advantages perfectly through cross-chain integration, bringing a new DeFi experience to global traders.
Unified cross-chain liquidity: breaking liquidity fragmentation
In traditional markets, liquidity between different chains is often fragmented, resulting in traders being unable to enjoy the best transaction price and depth. Vertex Edge was born in this context, and through a unified order book network, it has achieved synchronous sharing of permanent liquidity across multiple chains.
At present, Vertex Edge has covered perpetual contract liquidity on 7 mainstream chains including Arbitrum, Base, Sei, Blast, Mantle, Sonic and Abstract, so that traders no longer need to worry about the dispersion of liquidity, and trade at the best price, truly achieving seamless global liquidity.Hybrid order book trading: the perfect balance between ultra-fast matching and on-chain settlement
Vertex Edge adopts a hybrid order book trading model, and its core technologies include:
Off-chain order book matcher: Utilizes ultra-high-speed off-chain matching mechanism to achieve order matching, with a latency of only 5-15 milliseconds, comparable to most centralized exchanges;
On-chain risk engine and AMM: Risk management system and automated market maker (AMM) are deployed on each supported chain to ensure that orders can be settled in a secure and transparent manner after matching.
This architecture not only ensures extremely fast response times for transactions, but also provides users with decentralized security through on-chain settlement, allowing traders to enjoy CEX-level performance while maintaining the independence of asset self-custody.
- Unified cross-margin management: Utilize every penny of capital efficiently
Vertex platform provides users with unified cross-margin management functions. Whether it is spot, perpetual contracts or embedded funding markets, all assets and positions can be integrated into a unified margin account.
This design allows users to:
Multi-account function: manage multiple accounts in a single wallet to allocate funds more efficiently;
Leveraged spot positions: use all assets as margin to achieve higher capital efficiency;
Flexible risk management: consider deposits, positions and profit and loss data in a unified manner to control risk exposure accurately.
Connect Vertex
Log in to the "vertex protocol" page address:
Connect your wallet
Vertex is the same as most DEXs. After logging into the dapp, you need to connect to the wallet for authorization. Vertex's sub-account system is distinguished based on a label. The label participates in the wallet address calculation to obtain a sub-account wallet address. At the same time, this address is authorized to place orders and other operations.
For example, the wallet address when connected using WalletConnect is: 0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43
, the default label used is "default", and the sub-account address calculated is: 0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c4364656661756c7400000000000
. The default label is: 64656661756c7400000000000
.
Vertex supports charging into multiple assets. Generally, we can choose to top up USDC as margin and use the connected wallet to transfer transactions directly. It should be noted that operations such as withdrawing, transferring, sending tokens, and transferring sub-accounts on Vertex will consume USDC, and the fees are not low, so these operations need to be called with caution.
Switch to a different chain:
On vertex, different chains have different configuration information such as nodes, indexers, chain IDs, etc. The default chain when FMZ is encapsulated is Arbitrum One
, we can use Log(HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=contracts"))
to query a chain's ID, deployed contract information, etc.
Configure on FMZ Platform
After logging in to FMZ.COM, on the exchange configuration page, select "Cryptocurrency", select Vertex Exchange, you can configure the wallet proxy key on the dapp directly. Of course, you can also configure the wallet private key. On Vertex, you can use the interface to manage proxy key authorization/deauthorization and other operations, which is also convenient.
Proxy key:
For example, on the front-end page of the vertex DEX exchange: Chrome browser (turn on debugging) -> Application -> Local Storage -> https://app.vertex -> vertex userSettings
Configure Vertex exchange
There are two things that need to be configured on FMZ. The first is the wallet address (not the address of the proxy key used for signing, it must be the wallet address connected to the dapp). The second is the secret key used for signing (it can be the wallet private key or the proxy key). Since the proxy key can be used, the configured wallet address and secret key are not necessarily a pair.
The vertex sub-account system is identified by tags. On FMZ, the default main sub-account is used. If you need to switch, you can use the following in the code:
exchange.IO("subAccountTag", "default") // Switch to the main sub-account
exchange.IO("subAccountTag", "test01") // Switch to the sub-account with the label name test01
Practice on FMZ
After configuring the exchange configuration information and deploying a docker program that can access the vertex interface, we can start writing some code for practical operations.
We use the main sub-account for testing (the sub-account with the tag: default), and we use the Arbitrum network.
Tickers and chain information
- Obtain contract market information
function main() {
var markets = exchange.GetMarkets()
if (!markets) {
throw "get markets error"
}
var tbl = {
type: "table",
title: "test markets",
cols: [
"key", "Symbol", "BaseAsset", "QuoteAsset", "TickSize", "AmountSize", "PricePrecision", "AmountPrecision", "MinQty",
"MaxQty", "MinNotional", "MaxNotional", "CtVal", "CtValCcy"
],
rows: []
}
for (var symbol in markets) {
var market = markets[symbol]
tbl.rows.push([
symbol, market.Symbol, market.BaseAsset, market.QuoteAsset, market.TickSize, market.AmountSize,
market.PricePrecision, market.AmountPrecision, market.MinQty, market.MaxQty, market.MinNotional, market.MaxNotional, market.CtVal, market.CtValCcy
])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
return markets
}
It can be seen that the contract type on Vertex is USDC-based contract, the margin is USDC, and the value of one contract represents one corresponding currency. For example, BTC_USDC.swap
is the USDC-based contract of BTC, one represents one BTC position. The above code shows how to request contract market information and output various contents.
- Get depth information (order book)
function main() {
var depths = [{"symbol": "ETH_USDC"}, {"symbol": "SOL_USDC"}, {"symbol": "BTC_USDC"}]
for (var ele of depths) {
ele["depth"] = exchange.GetDepth(ele["symbol"] + ".swap")
}
var tbls = []
for (var ele of depths) {
var tbl = {"type": "table", "title": ele["symbol"], "cols": ["level", "price", "amount"], "rows": []}
var depth = ele["depth"]
for (var i = 0 ; i < 3 ; i++) {
tbl["rows"].push(["sell" + (i + 1), depth.Asks[i].Price, depth.Asks[i].Amount])
}
tbl["rows"].reverse()
for (var i = 0 ; i < 3 ; i++) {
tbl["rows"].push(["buy" + (i + 1), depth.Bids[i].Price, depth.Bids[i].Amount])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
Status information
- Order flow information of market transactions
function main() {
var arrTrades = [{"symbol": "ETH_USDC"}, {"symbol": "SOL_USDC"}, {"symbol": "BTC_USDC"}]
for (var ele of arrTrades) {
ele["trades"] = exchange.GetTrades(ele["symbol"] + ".swap")
}
var tbls = []
for (var ele of arrTrades) {
var tbl = {"type": "table", "title": ele["symbol"], "cols": ["Time", "Price", "Amount", "side"], "rows": []}
var trades = ele["trades"]
for (var trade of trades) {
tbl["rows"].push([_D(trade.Time), trade.Price, trade.Amount, trade.Type == 0 ? "buy" : "sell"])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
Status information
- K-line data
function main() {
let c = KLineChart({
overlay: true
})
let bars = exchange.GetRecords("SOL_USDC.swap")
if (!bars) {
return
}
bars.forEach(function(bar, index) {
c.begin(bar)
Log(index, bar)
c.close()
})
}
vertex chart
FMZ strategy operation chart
- Funding rate
function main() {
var fundings = exchange.GetFundings()
var tbl = {
"type": "table",
"title": "GetFundings",
"cols": ["Symbol", "Interval", "Time", "Rate"],
"rows": [],
}
for (var f of fundings) {
tbl["rows"].push([f.Symbol, f.Interval / 3600000, _D(f.Time), f.Rate * 100 + " %"])
}
LogStatus(_D(), "\n`" + JSON.stringify(tbl) + "`")
}
The funding rate period is 1 hour.
- Query the on-chain data and the maximum order volume for a given account and product
Max Order Size
Gets the max order size possible of a given product for a given subaccount.
function main() {
// GET [GATEWAY_REST_ENDPOINT]/query?type=max_order_size&product_id={product_id}&sender={sender}&price_x18={price_x18}&direction={direction}
// price_x18=3000000000000000000000 : 3000 USDC
// product_id=4 : ETH_USDC.swap
// sender=0x123 : e.g. 0x123
return HttpQuery("https://gateway.prod.vertexprotocol.com/query?type=max_order_size&product_id=4&sender=0x123&price_x18=3000000000000000000000&direction=short")
}
- The scaling factor is mostly: 1e18, so: 300000000000000000000000 is 3000 USDC.
- product_id=4, trading product ID, 4 is Ethereum contract, ETH_USDC.swap
- sender is the sub-account address calculated from the wallet address and sub-account tag.
The data returned by the final request: {"status":"success","data":{"max_order_size":"170536415320344899"},"request_type":"query_max_order_size"}
It can be seen that the current account's available assets for the Ethereum perpetual contract with a price of 3000, the maximum order quantity for a sell order is: 0.17 ETH
- Query the proxy key information of the wallet authorization
Linked Signer
Retrieves current linked signer of a provided subaccount
function main() {
return HttpQuery("https://gateway.prod.vertexprotocol.com/query?type=linked_signer&subaccount=0x123")
}
Authorization information found:
{"status":"success","data":{"linked_signer":"0x79119..."},"request_type":"query_linked_signer"}
"0x79119..." This address is the proxy address that authorizes the order transaction when the wallet is connected to the vertex front-end page. This authorization can be canceled or added (through API calls).
Trading
The following is the focus of this article. We have been busy for a long time just to conduct transactions simply and quickly on decentralized exchanges.
- Place an order
Test a simple trade and place a normal limit order.
function main() {
var id1 = exchange.CreateOrder("ETH_USDC.swap", "buy", 2000, 0.1)
var id2 = exchange.CreateOrder("SOL_USDC.swap", "buy", 60, 2)
Log("ETH_USDC.swap id1:", id1)
Log("SOL_USDC.swap id2:", id2)
var orders = exchange.GetOrders("USDC.swap")
var tbl = {type: "table", title: "test GetOrders", cols: ["Symbol", "Id", "Price", "Amount", "DealAmount", "AvgPrice", "Status", "Type", "Offset", "ContractType"], rows: []}
for (var order of orders) {
tbl.rows.push([order.Symbol, order.Id, order.Price, order.Amount, order.DealAmount, order.AvgPrice, order.Status, order.Type, order.Offset, order.ContractType])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
Status information
Log information
- Cancel the order
function main() {
var orders = exchange.GetOrders("USDC.swap")
var tbl = {type: "table", title: "test GetOrders", cols: ["Symbol", "Id", "Price", "Amount", "DealAmount", "AvgPrice", "Status", "Type", "Offset", "ContractType"], rows: []}
for (var order of orders) {
tbl.rows.push([order.Symbol, order.Id, order.Price, order.Amount, order.DealAmount, order.AvgPrice, order.Status, order.Type, order.Offset, order.ContractType])
exchange.CancelOrder(order.Id)
}
LogStatus("`" + JSON.stringify(tbl) + "`")
return exchange.GetOrders()
}
Status information
Log information
- Query positions
function main() {
// Use market orders to open positions
exchange.SetCurrency("ETH_USDC")
exchange.SetContractType("swap")
exchange.Buy(-1, 0.01)
var positions = exchange.GetPositions()
var tbl = {type: "table", title: "test GetPosition/GetPositions", cols: ["Symbol", "Amount", "Price", "FrozenAmount", "Type", "Profit", "Margin", "ContractType", "MarginLevel"], rows: []}
for (var p of positions) {
tbl.rows.push([p.Symbol, p.Amount, p.Price, p.FrozenAmount, p.Type, p.Profit, p.Margin, p.ContractType, p.MarginLevel])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
Status information
Log information
It should be noted that the data provided by the Vertex API deviates from the content displayed on the Vertex front-end page, mainly due to differences in the average holding price and the profit and loss of holdings. This has been fed back to the Vertex team and it may be updated later.
- Trigger Order
Since the Trigger Order of vertex is an independent endPoint, when using the exchange.IO
function to place a conditional order, we need to specify Trigger: https://trigger.prod.vertexprotocol.com
. Next, let's continue to practice operation.
- nonce : A nonce parameter needs to be passed in for the next Trigger Order. Vertex has calculation requirements, so for ease of use, a method is designed in the encapsulation to obtain the calculated nonce.
- expiration : The same requirement applies to the expiration parameter, and the calculated expiration also needs to be obtained.
function main() {
// isTrigger : true
var nonce = exchange.IO("nonce", true) // If the nonce used in a Trigger Order, you need to specify isTrigger: true
// flag , reduceOnly
var expiration = exchange.IO("expiration", "GTC", false) // Set the order to GTC type, not just reduce position
// params
var params = {
"place_order": {
"product_id": 4,
"order": {
"sender": "0x123...",
"priceX18": "4100000000000000000000",
"amount": "-100000000000000000",
"expiration": expiration,
"nonce": nonce
},
"trigger": {
"price_above": "4000000000000000000000"
}
}
}
return exchange.IO("api", "POST", "https://trigger.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
Under the trigger endpoint, there is also:
- List Trigger Orders Interface
- Cancel Trigger Orders
- Cancel Product Trigger Orders
The calling method is similar to Place Trigger Order, so I will not go into details here.
Sub-account management
- Switch sub-account tag
The default sub-account tag is: default
, we switch to a custom tag: subAcc02
.
function main() {
exchange.IO("subAccountTag", "subAcc02")
return exchange.GetAccount()
}
Function results
When transferring money to a sub-account address, Vertex will create this sub-account.
- Transfer funds to the Vertex sub-account
The nonce of the account needs to be queried and passed into the parameters of the transfer interface as a parameter.
https://gateway.prod.vertexprotocol.com/v1/query?
type=nonces&address=0x6B3f11d807809B0b1E5e3243df04a280d9F94bF4
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"transfer_quote": {
"tx": {
// default -> subAcc02
"sender": "0xabc...", // default
"recipient": "0xdef...", // subAcc02
"amount": "7000000000000000000",
"nonce": nonce
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
For example: 0xabc... corresponds to the sub-account address with tag default.
0xdef... corresponds to the sub-account address with tag subAcc02.
0x123... is the wallet address.
Withdrawal, minting/destruction of liquidity tokens, etc.
- Withdraw coins from Vertex to wallet
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"withdraw_collateral": {
"tx": {
"sender": "0xabc...", // default
"productId": 0, // USDC : 0 , precision : 6
"amount": "10000000", // 10 USDC
"nonce": nonce
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
Pay attention to the precision of USDC.
- Minting LP
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"mint_lp": {
"tx": {
"sender": "0xabc...", // default
"productId": 31, // USDT_USDC
"amountBase": "10000000000000000000",
"quoteAmountLow": "9999900000000000000",
"quoteAmountHigh": "10100000000000000000",
"nonce": nonce,
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
Minting LP tokens adds liquidity to the exchange pool for the trading pair USDT_USDC
.
- Destroy LP
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"burn_lp": {
"tx": {
"sender": "0xabc...", // default
"productId": 31, // USDT_USDC
"amount": "7500000000000000000",
"nonce": nonce,
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
Websocket interface
Websocket interface endpoint: wss://gateway.prod.vertexprotocol.com/v1/ws
.
- Subscribe to public interface
function main() {
// wss://gateway.prod.vertexprotocol.com/v1/subscribe
var params = {
"method": "subscribe",
"stream": {
"type": "trade",
"product_id": 4
},
"id": 10
}
var ws = Dial("wss://gateway.prod.vertexprotocol.com/v1/subscribe|enableCompression=true&payload=" + JSON.stringify(params))
if (!ws) {
return "Dial error"
}
for (var i = 0 ; i < 10 ; i++) {
var ret = ws.read()
Log(ret)
}
ws.close()
}
- Subscribe to private interface verification
Because the private interface needs to verify the signature, a function exchange.IO("signature", strPayload)
is added to calculate the signature.
function main() {
// wss://gateway.prod.vertexprotocol.com/v1/subscribe
var params = {
"method": "authenticate",
"id": 0,
"tx": {
"sender": "0x123...",
"expiration": String(new Date().getTime() + 1000 * 15)
}
}
var signature = exchange.IO("signature", JSON.stringify(params))
params["signature"] = signature
var ws = Dial("wss://gateway.prod.vertexprotocol.com/v1/subscribe|enableCompression=true&payload=" + JSON.stringify(params))
if (!ws) {
return "Dial error"
}
var ret = ws.read()
return ret
}
Common function encapsulation
- exchange.IO("nonce", isTrigger) : Calculate the nonce parameter, for example, the Trigger order interface requires this parameter.
- exchange.IO("expiration", flag, reduceOnly) : Calculate the expiration parameter, for example, the Trigger order interface requires this parameter.
- exchange.IO("indexerBase", url) : Switch the indexer endpoint url.
- exchange.IO("address", walletAddress) : Switch wallet address.
- exchange.IO("privateKey", key) : Switch the secret key used for signing. It can be set to wallet private key or proxy key (much safer).
- exchange.IO("subAccountTag", tag) : Switch to the sub-account tab.
- exchange.IO("signature", strPayload) : Calculates the signature.
END
The above tests are based on the latest docker. You need to download the latest docker to support the Vertex DEX aggregator.
Thank you for your support and thank you for reading.
From: Quantitative Practice of DEX Exchanges (3) - Vertex Protocol User Guide