Vertex Protocol
Vertex is a cross-chain DEX combining spot trading, perpetual contracts, and money markets (lend/borrow) under a unified cross-margin portfolio. The off-chain sequencer matches orders in 5-15ms and batches settlements on-chain, providing gas-free trading with MEV protection. Vertex Edge synchronizes orderbook liquidity across Arbitrum, Base, Mantle, Sei, Blast, Sonic, and other EVM chains — trades match cross-chain at the sequencer level and settle on each user's origin chain.
What You Probably Got Wrong
LLMs have stale training data. These are the most common mistakes.
- "Vertex is Arbitrum-only" → Vertex started on Arbitrum but Vertex Edge now unifies liquidity across Arbitrum, Base, Mantle, Sei, Blast, Sonic, and more. The sequencer aggregates orders cross-chain and settles locally on each origin chain.
- "Product IDs are just 1, 2, 3" → Product ID 0 is USDC (the quote asset). Spot products use odd IDs (1=wBTC, 3=wETH, 5=wARB...). Perps use even IDs (2=BTC-PERP, 4=ETH-PERP, 6=ARB-PERP...). Getting this wrong sends orders to the wrong market.
- "Prices are in float" → All prices and amounts use fixed-point x18 representation (multiply by 10^18). The SDK provides
to_x18()andto_pow_10()helpers. Passing raw floats produces nonsensical orders. - "Signing is standard EIP-712" → Vertex uses EIP-712 typed data but the
senderfield isbytes32— a 20-byte address concatenated with a 12-byte subaccount identifier. The SDK handles this withsubaccount_to_bytes32(), but raw API callers must construct it correctly. - "Gas-free means no wallet interaction" → You still sign EIP-712 typed data per order. The sequencer batches and submits the on-chain transaction, so you pay no gas — but you must sign every execute action.
- "Money markets are a separate product" → Lending/borrowing is embedded. Depositing any spot asset automatically earns interest. Borrowing happens implicitly when your spot balance goes negative (e.g., selling spot you don't hold). There is no separate "borrow" action.
pip install vertex→ The package name isvertex-protocol, notvertex.pip install vertexinstalls an unrelated package.- "Nonces are sequential" → Order nonces are NOT sequential counters. Use
gen_order_nonce()from the SDK — it encodes a timestamp-based random nonce. Using sequential integers causes order rejection. - "OrderType.DEFAULT is a market order" →
OrderType.DEFAULTis a limit GTC (good-till-cancel) order. For IOC (immediate-or-cancel / market-like), useOrderType.IOC. For post-only (maker), useOrderType.POST_ONLY.
Architecture Overview
┌──────────────────────────────────────────────────┐
│ Vertex Edge │
│ (Cross-Chain Sequencer Layer) │
│ │
│ Arbitrum ─┐ │
│ Base ─────┤ │
│ Mantle ───┼── Unified CLOB ── 5-15ms matching │
│ Sei ──────┤ │
│ Blast ────┤ │
│ Sonic ────┘ │
│ │
│ ┌─────────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Spot Markets │ │ Perps │ │Money Market│ │
│ │ (odd IDs) │ │(even IDs)│ │ (embedded) │ │
│ └─────────────┘ └──────────┘ └────────────┘ │
│ │
│ Cross-Margin Portfolio (unified collateral) │
└──────────────┬───────────────────────────────────┘
│ On-chain settlement per origin chain
▼
Smart Contracts (risk engine, clearing)
Key concepts:
- Sequencer: Off-chain CLOB node. Receives signed orders, matches at 5-15ms latency, batches settlement on-chain. Gas costs are absorbed by the protocol.
- Vertex Edge: Splits sequencer state across chains. A user on Base and a user on Arbitrum can be matched in the same orderbook. Settlement happens on each user's origin chain.
- Cross-margin: All spot deposits, perp positions, and money market balances contribute to a single margin health calculation per subaccount.
- Subaccounts: Each wallet can create multiple subaccounts (identified by a 12-byte name appended to the address). Subaccounts isolate margin — a liquidation in one does not affect another.
Installation
pip install vertex-protocol
Or with uv:
uv pip install vertex-protocol
Verify:
import vertex_protocol
print(vertex_protocol.__version__)
Client Setup
from vertex_protocol.client import create_vertex_client, VertexClientMode
client = create_vertex_client(VertexClientMode.MAINNET, private_key)
Available modes:
| Mode | Value | Description |
|---|---|---|
| Mainnet (Arbitrum) | VertexClientMode.MAINNET |
Production on Arbitrum |
| Testnet | VertexClientMode.SEPOLIA_TESTNET |
Arbitrum Sepolia testnet |
| Base Mainnet | VertexClientMode.BASE_MAINNET |
Production on Base |
| Mantle Mainnet | VertexClientMode.MANTLE_MAINNET |
Production on Mantle |
| Sei Mainnet | VertexClientMode.SEI_MAINNET |
Production on Sei |
| Blast Mainnet | VertexClientMode.BLAST_MAINNET |
Production on Blast |
| Sonic Mainnet | VertexClientMode.SONIC_MAINNET |
Production on Sonic |
Product ID System
Products are grouped into health groups. Each group pairs a spot asset with its perp market.
| Product ID | Type | Market | Health Group |
|---|---|---|---|
| 0 | Quote | USDC | — |
| 1 | Spot | wBTC/USDC | BTC |
| 2 | Perp | BTC-PERP | BTC |
| 3 | Spot | wETH/USDC | ETH |
| 4 | Perp | ETH-PERP | ETH |
| 5 | Spot | wARB/USDC | ARB |
| 6 | Perp | ARB-PERP | ARB |
| 7 | Spot | wOP/USDC | OP |
| 8 | Perp | OP-PERP | OP |
| 31 | Spot | wSOL/USDC | SOL |
| 32 | Perp | SOL-PERP | SOL |
Rule: Odd IDs = spot, Even IDs = perps, 0 = USDC quote. Query the full list at runtime:
products = client.context.engine_client.get_all_products()
for product in products.spot_products:
print(f"Spot ID={product.product_id} — {product.book_info}")
for product in products.perp_products:
print(f"Perp ID={product.product_id} — {product.book_info}")
Utility Functions
Vertex uses x18 fixed-point math. The SDK provides essential helpers:
from vertex_protocol.utils.math import to_x18, to_pow_10, from_x18
from vertex_protocol.utils.nonce import gen_order_nonce
from vertex_protocol.utils.expiration import get_expiration_timestamp, OrderType
from vertex_protocol.utils.subaccount import SubaccountParams, subaccount_to_bytes32
# Price: $2000.50 → x18 representation
price = to_x18(2000.5)
# Amount: 0.1 ETH → integer with 18 decimals
amount = to_pow_10(1, 17) # 1 * 10^17 = 0.1 * 10^18
# Negative amount = sell
sell_amount = -to_pow_10(1, 17)
# Order nonce — timestamp-based, not sequential
nonce = gen_order_nonce()
# Expiration with order type
import time
expiration = get_expiration_timestamp(
OrderType.POST_ONLY,
int(time.time()) + 3600 # 1 hour
)
# Subaccount bytes32
sender = subaccount_to_bytes32(
SubaccountParams(
subaccount_owner="0xYourAddress",
subaccount_name="default"
)
)
Placing Orders
Spot Limit Order
import time
from vertex_protocol.engine_client.types.execute import (
PlaceOrderParams,
OrderParams,
)
from vertex_protocol.utils.math import to_x18, to_pow_10
from vertex_protocol.utils.nonce import gen_order_nonce
from vertex_protocol.utils.expiration import get_expiration_timestamp, OrderType
from vertex_protocol.utils.subaccount import SubaccountParams
owner = client.context.engine_client.signer.address
order = OrderParams(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
priceX18=to_x18(65000),
amount=to_pow_10(1, 16), # 0.01 wBTC (positive = buy)
expiration=get_expiration_timestamp(OrderType.DEFAULT, int(time.time()) + 86400),
nonce=gen_order_nonce(),
)
res = client.market.place_order(
PlaceOrderParams(product_id=1, order=order)
)
print(f"Order digest: {res.digest}")
Perp Market Order (IOC)
order = OrderParams(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
priceX18=to_x18(70000), # worst acceptable price (slippage bound)
amount=-to_pow_10(1, 17), # -0.1 BTC = short
expiration=get_expiration_timestamp(OrderType.IOC, int(time.time()) + 60),
nonce=gen_order_nonce(),
)
res = client.market.place_order(
PlaceOrderParams(product_id=2, order=order)
)
Cancel Orders
from vertex_protocol.engine_client.types.execute import CancelOrdersParams
res = client.market.cancel_orders(
CancelOrdersParams(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
product_ids=[1, 2],
digests=["0xorderdigest1...", "0xorderdigest2..."],
nonce=gen_order_nonce(),
)
)
Cancel and Replace (Atomic)
from vertex_protocol.engine_client.types.execute import CancelAndPlaceParams
res = client.market.cancel_and_place(
CancelAndPlaceParams(
cancel_orders=CancelOrdersParams(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
product_ids=[2],
digests=["0xoldorderdigest..."],
nonce=gen_order_nonce(),
),
place_order=PlaceOrderParams(
product_id=2,
order=OrderParams(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
priceX18=to_x18(66000),
amount=to_pow_10(1, 17),
expiration=get_expiration_timestamp(
OrderType.POST_ONLY, int(time.time()) + 3600
),
nonce=gen_order_nonce(),
),
),
)
)
Querying Data
Subaccount Info
from vertex_protocol.utils.subaccount import SubaccountParams
info = client.context.engine_client.get_subaccount_info(
subaccount=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
)
)
print(f"Exists: {info.exists}")
print(f"Health (initial): {info.healths.initial}")
print(f"Health (maintenance): {info.healths.maintenance}")
for balance in info.spot_balances:
print(f"Spot product {balance.product_id}: {balance.balance}")
for balance in info.perp_balances:
print(f"Perp product {balance.product_id}: {balance.balance}")
Market Prices
market_price = client.context.engine_client.get_market_price(product_id=2)
print(f"BTC-PERP bid: {market_price.bid}")
print(f"BTC-PERP ask: {market_price.ask}")
All Products
products = client.context.engine_client.get_all_products()
for spot in products.spot_products:
print(f"ID {spot.product_id}: {spot.book_info}")
Fee Rates
fees = client.context.engine_client.get_fee_rates(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
)
)
print(f"Maker fee: {fees.maker_fee_rate}")
print(f"Taker fee: {fees.taker_fee_rate}")
Deposits and Withdrawals
Deposit Collateral
Deposits go through the on-chain Endpoint contract. Approve USDC first, then deposit:
from vertex_protocol.contracts.types import DepositCollateralParams
approve_tx = client.spot.approve_allowance(
product_id=0, # USDC
amount=to_pow_10(1000, 6), # 1000 USDC (6 decimals on-chain)
)
print(f"Approve tx: {approve_tx}")
deposit_tx = client.spot.deposit(
DepositCollateralParams(
subaccount_name="default",
product_id=0,
amount=to_pow_10(1000, 6),
)
)
print(f"Deposit tx: {deposit_tx}")
Withdraw Collateral
Withdrawals are signed executes processed by the sequencer:
from vertex_protocol.engine_client.types.execute import WithdrawCollateralParams
res = client.spot.withdraw(
WithdrawCollateralParams(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
product_id=0,
amount=to_pow_10(500, 6),
nonce=gen_order_nonce(),
)
)
Money Markets (Lend/Borrow)
Lending and borrowing are implicit — no separate actions needed:
- Lending: Depositing any spot asset automatically earns the prevailing interest rate. Your idle USDC, wETH, or wBTC generates yield passively.
- Borrowing: When your spot balance for an asset goes negative (by selling spot you don't hold, or by explicit borrow), you automatically pay the borrow rate. Your cross-margin health must remain positive.
Check interest rates:
products = client.context.engine_client.get_all_products()
for spot in products.spot_products:
print(
f"Product {spot.product_id}: "
f"deposit APR={spot.product.long_weight_initial}, "
f"borrow APR={spot.product.short_weight_initial}"
)
Subscriptions (WebSocket Streams)
Connect to the subscriptions endpoint for real-time data:
import json
import websocket
WS_URL = "wss://gateway.prod.vertexprotocol.com/v1/subscribe"
def on_message(ws, message):
data = json.loads(message)
print(data)
def on_open(ws):
ws.send(json.dumps({
"method": "subscribe",
"stream": {
"type": "best_bid_offer",
"product_id": 2,
},
}))
ws = websocket.WebSocketApp(
WS_URL,
on_message=on_message,
on_open=on_open,
)
ws.run_forever()
Available stream types:
| Stream Type | Fields | Description |
|---|---|---|
best_bid_offer |
product_id |
Top of book bid/ask |
trade |
product_id |
Executed trades |
book_depth |
product_id |
Full orderbook snapshot |
fill |
product_id, subaccount |
Your order fills |
position_change |
subaccount |
Position updates |
order_update |
product_id, subaccount |
Order status changes |
Trigger Orders (Stop Loss / Take Profit)
Trigger orders are submitted to a separate trigger endpoint and execute when price conditions are met:
import requests
import json
TRIGGER_URL = "https://trigger.prod.vertexprotocol.com/v1"
trigger_payload = {
"place_order": {
"product_id": 2,
"order": {
"sender": sender_bytes32,
"priceX18": str(to_x18(60000)),
"amount": str(-to_pow_10(1, 17)), # sell 0.1 BTC
"expiration": str(get_expiration_timestamp(
OrderType.DEFAULT, int(time.time()) + 86400 * 30
)),
"nonce": str(gen_order_nonce()),
},
"trigger": {
"price_above": None,
"price_below": str(to_x18(62000)), # trigger when price drops below 62k
},
"signature": "0x...", # EIP-712 signature
}
}
resp = requests.post(
f"{TRIGGER_URL}/execute",
json=trigger_payload,
headers={"Content-Type": "application/json"},
)
print(resp.json())
Indexer (Historical Data)
The archive/indexer API provides historical data not available from the gateway:
matches = client.context.indexer_client.get_matches(
subaccount=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
product_ids=[2, 4],
limit=50,
)
for match in matches:
print(f"Product {match.product_id}: {match.order.amount} @ {match.order.price}")
Available indexer queries:
| Method | Description |
|---|---|
get_matches |
Historical fills for a subaccount |
get_orders |
Historical orders placed |
get_funding_rate |
24h funding rate for perp products |
get_interest_and_funding_payments |
Interest/funding payment history |
get_linked_signer |
Current linked signer for a subaccount |
get_subaccounts |
List all subaccounts for a wallet |
get_rewards |
Trading rewards earned |
get_candlesticks |
OHLCV candle data |
Linked Signers
A linked signer is a secondary key authorized to sign on behalf of your subaccount. Useful for bot trading — keep your main key in cold storage and link a hot key with rate limits:
from vertex_protocol.engine_client.types.execute import LinkSignerParams
res = client.context.engine_client.link_signer(
LinkSignerParams(
sender=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
),
signer=SubaccountParams(
subaccount_owner="0xHotWalletAddress",
subaccount_name="default",
),
nonce=gen_order_nonce(),
)
)
API Endpoints Reference
Arbitrum (Mainnet)
| Service | URL |
|---|---|
| Gateway REST | https://gateway.prod.vertexprotocol.com/v1 |
| Gateway WebSocket | wss://gateway.prod.vertexprotocol.com/v1/ws |
| Subscriptions | wss://gateway.prod.vertexprotocol.com/v1/subscribe |
| Indexer/Archive | https://archive.prod.vertexprotocol.com/v1 |
| Trigger | https://trigger.prod.vertexprotocol.com/v1 |
Base
| Service | URL |
|---|---|
| Gateway REST | https://gateway.base-prod.vertexprotocol.com/v1 |
| Gateway WebSocket | wss://gateway.base-prod.vertexprotocol.com/v1/ws |
| Subscriptions | wss://gateway.base-prod.vertexprotocol.com/v1/subscribe |
| Indexer/Archive | https://archive.base-prod.vertexprotocol.com/v1 |
| Trigger | https://trigger.base-prod.vertexprotocol.com/v1 |
Testnet (Sepolia)
| Service | URL |
|---|---|
| Gateway REST | https://gateway.sepolia-test.vertexprotocol.com/v1 |
| Gateway WebSocket | wss://gateway.sepolia-test.vertexprotocol.com/v1/ws |
| Subscriptions | wss://gateway.sepolia-test.vertexprotocol.com/v1/subscribe |
| Indexer/Archive | https://archive.sepolia-test.vertexprotocol.com/v1 |
| Trigger | https://trigger.sepolia-test.vertexprotocol.com/v1 |
Common Patterns
Check Health Before Trading
info = client.context.engine_client.get_subaccount_info(
subaccount=SubaccountParams(
subaccount_owner=owner,
subaccount_name="default",
)
)
initial_health = float(info.healths.initial.health)
if initial_health < 0:
print("Account unhealthy — reduce positions or add collateral")
Poll for Order Fill
import time
digest = res.digest
for _ in range(30):
order = client.context.engine_client.get_order(
product_id=2,
digest=digest,
)
if order.status == "filled":
print("Order filled")
break
time.sleep(1)
Multi-Subaccount Isolation
hedge_sub = SubaccountParams(subaccount_owner=owner, subaccount_name="hedge")
spec_sub = SubaccountParams(subaccount_owner=owner, subaccount_name="spec")