1. dYdX v4 Cosmos Chain Architecture
dYdX v4 represents a fundamental architectural departure from all previous versions. Where dYdX v3 ran on StarkEx (a ZK-rollup on Ethereum), v4 is a fully sovereign Cosmos SDK blockchain — the dYdX Chain — with its own validator set, native token (DYDX), and block production schedule.
This shift was motivated by performance requirements that no Ethereum L2 could satisfy at launch: dYdX needs to process thousands of order book updates per second with sub-second latency, and Cosmos appchain architecture allows the protocol to run an in-memory CLOB directly in the validators' memory without writing every order to the chain.
Consensus and Validator Set
The dYdX Chain uses CometBFT (Tendermint) consensus with a validator set initially capped at 60 validators, growing over time via governance. Validators must stake DYDX and are slashed for misbehavior. Block finality is single-round (no probabilistic finality), making it suitable for agents that need to confirm fills before acting on them.
Network Endpoints
# REST API (indexer) — for reading state
https://indexer.dydx.trade/v4
# gRPC node — for submitting transactions
https://dydx-grpc.publicnode.com:443
# WebSocket (indexer) — real-time feeds
wss://indexer.dydx.trade/v4/ws
# Testnet (use for agent development)
https://indexer.v4testnet.dydx.exchange/v4
wss://indexer.v4testnet.dydx.exchange/v4/ws
2. The Subaccount Model
dYdX v4 introduces a subaccount system that is critical for agent designs. Each wallet address can create up to 128,000 numbered subaccounts (subaccount 0, 1, 2, ...). Each subaccount has its own isolated margin — a position loss in subaccount 1 does not affect subaccount 2's collateral.
Why Subaccounts Matter for Agents
- Strategy isolation: Run a momentum strategy in subaccount 0, a funding harvest strategy in subaccount 1, and market making in subaccount 2 — all under one wallet, with independent risk budgets.
- Multi-agent systems: A coordinator agent can allocate capital across specialist sub-agents, each operating a dedicated subaccount, without needing separate private keys.
- Risk containment: A bug in one strategy can liquidate one subaccount without cascading to others — crucial for production agent deployments.
from dydx_v4_client import NodeClient, IndexerClient
from dydx_v4_client.network import MAINNET
from dydx_v4_client.wallet import Wallet
# Initialize wallet from mnemonic
wallet = await Wallet.from_mnemonic(
mnemonic="word1 word2 ... word24"
)
# Subaccount numbers
MOMENTUM_SUBACCOUNT = 0
FUNDING_SUBACCOUNT = 1
MARKET_MAKING_SUBACCOUNT = 2
# Query subaccount state
indexer = IndexerClient(MAINNET.rest_indexer)
state = indexer.account.get_subaccount(
address=wallet.address,
subaccount_number=MOMENTUM_SUBACCOUNT
)
print(f"Equity: ${state['subaccount']['equity']}")
print(f"Free collateral: ${state['subaccount']['freeCollateral']}")
Deposits and Withdrawals
dYdX v4 accepts USDC deposits from multiple chains via the Noble USDC bridge (Cosmos IBC) or via the official bridge from Ethereum/Arbitrum. The withdrawal process is also non-custodial and bridge-based — withdrawals take approximately 30 minutes for the Noble bridge and longer for EVM bridges.
3. Order Types and Fee Structure
dYdX v4 supports a rich set of order types that cover most algorithmic trading needs:
- Limit (GTC): Standard limit order, remains in book until filled or cancelled. Suitable for maker strategies.
- Limit (GTT): Good-Till-Time — expires at a specified block height. Useful for agents that need guaranteed expiry without an active cancel call.
- Limit (IOC): Immediate-Or-Cancel — fills what it can against current liquidity, cancels the rest. For taker strategies that need predictable execution.
- Limit (FOK): Fill-Or-Kill — must fill completely or not at all. Used for block trades.
- Market: Crosses spread immediately. Uses a limit price with significant slippage tolerance (configured by caller).
- Stop Limit / Stop Market: Conditional orders triggered by oracle/index price. Stored in the in-memory order book with conditional execution logic.
- Trailing Stop: Stop price trails market price by a fixed amount or percentage.
- Take Profit: Limit order that executes when price reaches a target, closing or reducing a position.
Fee Tiers
| Tier | 30-Day Volume | Maker Fee | Taker Fee |
|---|---|---|---|
| 1 (Base) | < $1M | 0.000% (zero!) | 0.050% |
| 2 | $1M–$5M | 0.000% | 0.045% |
| 3 | $5M–$25M | -0.005% | 0.040% |
| 4 (Market Maker) | > $25M | -0.011% | 0.035% |
| DYDX Staker bonus | Any | Additional -0.002% | -0.003% |
4. Python Client Integration
dYdX provides an official Python client library (dydx-v4-client) that wraps the gRPC transaction submission and REST indexer queries. Installation is straightforward:
pip install dydx-v4-client
Complete Agent Setup
import asyncio
from dydx_v4_client import NodeClient, IndexerClient
from dydx_v4_client.network import MAINNET
from dydx_v4_client.wallet import Wallet
from dydx_v4_client.node.market import Market
from dydx_v4_client.indexer.markets import MarketStatus
from v4_proto.dydxprotocol.clob.order_pb2 import Order
class DydxAgent:
def __init__(self, mnemonic: str, subaccount: int = 0):
self.mnemonic = mnemonic
self.subaccount_num = subaccount
self.node = None
self.indexer = IndexerClient(MAINNET.rest_indexer)
self.wallet = None
async def connect(self):
self.wallet = await Wallet.from_mnemonic(self.mnemonic)
self.node = await NodeClient.connect(MAINNET)
print(f"Connected. Address: {self.wallet.address}")
async def get_market_info(self, ticker: str) -> dict:
markets = self.indexer.markets.get_perpetual_markets(ticker)
return markets['markets'][ticker]
async def place_limit_order(
self,
market: str,
side: str, # "BUY" or "SELL"
size: float,
price: float,
good_till_block_offset: int = 20
):
market_info = await self.get_market_info(market)
ticker_id = market_info['clobPairId']
quantum_conv = Market(market_info)
current_block = await self.node.latest_block_height()
order_id = self.wallet.order_id(
address=self.wallet.address,
subaccount_number=self.subaccount_num,
client_id=await self.node.latest_block_height(),
order_flags=Order.ORDER_FLAG_LONG_TERM
)
order = self.node.place_order(
wallet=self.wallet,
order=Order(
order_id=order_id,
side=Order.SIDE_BUY if side == "BUY" else Order.SIDE_SELL,
quantums=quantum_conv.quantums(size),
subticks=quantum_conv.subticks(price),
good_til_block_time=current_block + good_till_block_offset,
time_in_force=Order.TIME_IN_FORCE_UNSPECIFIED,
reduce_only=False,
client_metadata=0,
condition_type=Order.CONDITION_TYPE_UNSPECIFIED
)
)
return await order
async def get_positions(self) -> list:
state = self.indexer.account.get_subaccount(
self.wallet.address, self.subaccount_num
)
return state['subaccount'].get('openPerpetualPositions', {})
# Usage
async def main():
agent = DydxAgent(
mnemonic="your 24-word mnemonic here",
subaccount=0
)
await agent.connect()
# Buy 0.01 BTC at $62,000
result = await agent.place_limit_order(
market="BTC-USD",
side="BUY",
size=0.01,
price=62000.0
)
print(f"Order placed: {result}")
asyncio.run(main())
5. Market Making on dYdX
Market making on dYdX v4 is attractive for agents because of the zero maker fee at Tier 1 and the ability to cancel orders instantly (off-chain, in the mempool). The key challenge is managing inventory risk — when your bid or ask fills, you hold an unhedged position until you can hedge it or the other side fills.
Simple Inventory-Aware Market Maker
import asyncio
from dataclasses import dataclass
from dydx_v4_client import IndexerClient
from dydx_v4_client.network import MAINNET
@dataclass
class MMParams:
market: str = "ETH-USD"
base_spread_bps: float = 5.0
max_position_usd: float = 5000.0
order_size_usd: float = 500.0
inventory_skew_factor: float = 0.5 # skew quotes based on inventory
class InventoryAwareMM:
def __init__(self, params: MMParams, agent):
self.params = params
self.agent = agent
self.indexer = IndexerClient(MAINNET.rest_indexer)
self.inventory_usd = 0.0
def compute_quotes(self, mid: float) -> tuple:
# Skew spread based on inventory
inventory_ratio = self.inventory_usd / self.params.max_position_usd
skew = mid * (self.params.inventory_skew_factor * inventory_ratio) / 10000
half_spread = mid * self.params.base_spread_bps / 20000
bid = mid - half_spread + skew # long inventory: raise bid to sell
ask = mid + half_spread + skew # short inventory: lower ask to buy
size = self.params.order_size_usd / mid
return round(bid, 2), round(ask, 2), round(size, 4)
async def get_mid(self) -> float:
ob = self.indexer.markets.get_perpetual_market_orderbook(self.params.market)
best_bid = float(ob['bids'][0]['price'])
best_ask = float(ob['asks'][0]['price'])
return (best_bid + best_ask) / 2
async def update_inventory(self):
positions = await self.agent.get_positions()
pos = positions.get(self.params.market)
if pos:
self.inventory_usd = float(pos['unrealizedPnl']) + float(pos['size']) * float(pos['entryPrice'])
else:
self.inventory_usd = 0.0
async def run(self):
while True:
try:
await self.update_inventory()
mid = await self.get_mid()
bid, ask, size = self.compute_quotes(mid)
print(f"{self.params.market} | bid={bid} ask={ask} size={size} inv=${self.inventory_usd:.0f}")
# Place orders (cancel-replace pattern)
await self.agent.place_limit_order(self.params.market, "BUY", size, bid)
await self.agent.place_limit_order(self.params.market, "SELL", size, ask)
except Exception as e:
print(f"MM error: {e}")
await asyncio.sleep(15)
6. Staking DYDX for Fee Discounts
The DYDX token serves as the governance and staking token of the dYdX Chain. Agents that stake DYDX receive additional fee discounts beyond the volume tier — a valuable consideration for agents trading consistently at moderate volumes.
Staking Mechanics
- Staking is done by delegating DYDX to one or more validators using a standard Cosmos SDK delegation transaction.
- The 30-day unbonding period means staked DYDX cannot be withdrawn for a month — plan capital allocation accordingly.
- Staking rewards (from protocol revenue and inflation) compound automatically if redelegated.
- Validators charge a commission (typically 5–10%) on rewards distributed to delegators.
from dydx_v4_client import NodeClient
from dydx_v4_client.network import MAINNET
from dydx_v4_client.wallet import Wallet
from cosmos.staking.v1beta1.tx_pb2 import MsgDelegate
from cosmos.base.v1beta1.coin_pb2 import Coin
async def stake_dydx(mnemonic: str, validator_address: str, amount_dydx: float):
wallet = await Wallet.from_mnemonic(mnemonic)
node = await NodeClient.connect(MAINNET)
# Convert DYDX to adydx (smallest unit, 1e18)
amount_adydx = int(amount_dydx * 1e18)
delegate_msg = MsgDelegate(
delegator_address=wallet.address,
validator_address=validator_address,
amount=Coin(denom="adydx", amount=str(amount_adydx))
)
tx_hash = await node.broadcast(
wallet=wallet,
messages=[delegate_msg],
memo="purple-flea-agent-stake"
)
print(f"Staked {amount_dydx} DYDX | tx: {tx_hash}")
return tx_hash
# Example: stake 100 DYDX to earn fee discounts
# dydxvaloper1... is the validator's operator address
asyncio.run(stake_dydx(
mnemonic="your mnemonic",
validator_address="dydxvaloper1xyz...",
amount_dydx=100.0
))
Estimated Annual Staking APR
| Amount Staked | Approx APR | Fee Discount |
|---|---|---|
| 100 DYDX | ~12% | -0.002% maker |
| 1,000 DYDX | ~12% | -0.003% taker |
| 10,000 DYDX | ~12% | Max discount tier |
7. WebSocket Feeds for Real-Time Agent Data
dYdX provides a rich WebSocket interface through the indexer service. Agents should subscribe to WebSocket feeds rather than polling REST endpoints for performance-sensitive strategies.
import asyncio
import json
import websockets
DYDX_WS = "wss://indexer.dydx.trade/v4/ws"
async def subscribe_orderbook_and_fills(market: str, address: str):
async with websockets.connect(DYDX_WS) as ws:
# Subscribe to order book snapshots + updates
await ws.send(json.dumps({
"type": "subscribe",
"channel": "v4_orderbook",
"id": market
}))
# Subscribe to account fill events
await ws.send(json.dumps({
"type": "subscribe",
"channel": "v4_accounts",
"id": f"{address}/0" # address/subaccount_number
}))
async for message in ws:
data = json.loads(message)
channel = data.get("channel")
msg_type = data.get("type")
if channel == "v4_orderbook" and msg_type == "channel_batch_data":
updates = data["contents"]
bids = updates.get("bids", [])
asks = updates.get("asks", [])
if bids:
print(f"OB update — best bid: ${bids[0]['price']}")
if asks:
print(f"OB update — best ask: ${asks[0]['price']}")
elif channel == "v4_accounts" and msg_type == "channel_data":
contents = data["contents"]
if "fills" in contents:
for fill in contents["fills"]:
print(f"FILL: {fill['side']} {fill['size']} {fill['market']} @ ${fill['price']}")
print(f" Fee: ${fill['fee']} | Liquidity: {fill['liquidity']}")
asyncio.run(subscribe_orderbook_and_fills("BTC-USD", "dydx1abc..."))
8. dYdX v4 vs Purple Flea Perpetuals
dYdX v4 is a powerful perpetuals exchange for agents that are comfortable with the Cosmos ecosystem, DYDX staking, and the bridge-based deposit/withdrawal flow. Purple Flea provides a comparison point that is specifically optimized for AI agents at all capital levels:
| Feature | dYdX v4 | Purple Flea |
|---|---|---|
| Perpetual markets | ~60 | 275+ |
| Maker fee (base tier) | 0.000% (zero!) | 0.05% (simple flat) |
| Deposit method | Bridge (30 min delay) | Direct + faucet |
| Withdrawal delay | 30 min (bridge) | Instant |
| MCP native tools | No | Yes |
| Agent faucet (free start) | No | Yes — faucet.purpleflea.com |
| Subaccounts | Yes (128k per wallet) | Single account model |
| Multi-service (casino, escrow, wallet) | No | 6-product suite |
| 3-level referral income | Standard referral only | Yes — up to 15% of fees |
| Staking for fee discount | Yes (DYDX staking) | Volume-based tiers |
Cross-Venue Opportunities
The most sophisticated autonomous agents trade both venues simultaneously:
- Funding arbitrage: Monitor funding rates on both dYdX and Purple Flea for the same asset. When a large divergence appears, go long on the lower-rate side and short on the higher-rate side — capturing the spread as delta-neutral yield.
- Liquidity migration: Post limit orders on dYdX (zero maker fee), and when filled, immediately route the hedge to Purple Flea's deeper market book for markets that are illiquid on dYdX.
- Basis trading: Exploit price differences between dYdX spot-equivalent pricing and Purple Flea's perpetual markets on the same underlying asset.
9. Complete dYdX Trading Agent
Here is a full example of a trend-following agent that runs on dYdX v4, uses the indexer WebSocket for real-time price data, and implements basic risk controls:
import asyncio
from collections import deque
from dydx_v4_client import IndexerClient
from dydx_v4_client.network import MAINNET
class TrendAgent:
def __init__(self, agent, market="ETH-USD", lookback=20):
self.agent = agent
self.market = market
self.prices = deque(maxlen=lookback)
self.position = 0.0
self.max_size = 0.1 # ETH
self.indexer = IndexerClient(MAINNET.rest_indexer)
def sma(self, window: int) -> float:
if len(self.prices) < window:
return None
data = list(self.prices)[-window:]
return sum(data) / window
async def fetch_price(self) -> float:
trades = self.indexer.markets.get_perpetual_market_trades(
self.market, limit=1
)
return float(trades['trades'][0]['price'])
async def step(self):
price = await self.fetch_price()
self.prices.append(price)
sma5 = self.sma(5)
sma20 = self.sma(20)
if sma5 is None or sma20 is None:
print(f"Warming up... ({len(self.prices)} prices)")
return
signal = "LONG" if sma5 > sma20 else "SHORT"
print(f"{self.market} ${price:.2f} | SMA5={sma5:.2f} SMA20={sma20:.2f} | Signal: {signal}")
if signal == "LONG" and self.position <= 0:
if self.position < 0:
# Close short
await self.agent.place_limit_order(self.market, "BUY", abs(self.position), price * 1.001)
await self.agent.place_limit_order(self.market, "BUY", self.max_size, price * 0.999)
self.position = self.max_size
elif signal == "SHORT" and self.position >= 0:
if self.position > 0:
# Close long
await self.agent.place_limit_order(self.market, "SELL", self.position, price * 0.999)
await self.agent.place_limit_order(self.market, "SELL", self.max_size, price * 1.001)
self.position = -self.max_size
async def run(self):
while True:
try:
await self.step()
except Exception as e:
print(f"Agent error: {e}")
await asyncio.sleep(60) # 1-minute bars
Try Purple Flea's 275+ Markets
MCP-native perpetuals trading for AI agents. Free faucet for new agents, no bridge required, 3-level referral income.
Register Your Agent Claim Free Funds