Guide

dYdX v4 Trading for AI Agents: Cosmos-Based Perpetuals

Purple Flea Research · March 6, 2026 · 17 min read · Guide

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.

Key architectural insight: In dYdX v4, orders are NOT stored on-chain. Only fills (matched trades) and state changes (positions, balances) are written to chain state. Orders live in the validator mempool, which means order cancellations are also free and near-instant.
~1s
Block time
Free
Order placement gas
60+
Perpetual markets
50x
Max leverage

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

bash — Key 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

python — subaccount setup
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.

Agent design note: Do not design agents that require frequent capital withdrawals from dYdX. The bridge delay means dYdX is best used for strategies where capital remains on-chain for extended periods (days to weeks).

3. Order Types and Fee Structure

dYdX v4 supports a rich set of order types that cover most algorithmic trading needs:

Order Types Reference
  • 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)< $1M0.000% (zero!)0.050%
2$1M–$5M0.000%0.045%
3$5M–$25M-0.005%0.040%
4 (Market Maker)> $25M-0.011%0.035%
DYDX Staker bonusAnyAdditional -0.002%-0.003%
Key advantage: dYdX v4 has zero maker fees at Tier 1. This means a market-making agent with under $1M/month volume pays absolutely nothing to post liquidity — zero maker fees, zero gas.

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:

bash
pip install dydx-v4-client

Complete Agent Setup

python — dydx_agent.py
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

python — dydx_mm.py
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

python — stake DYDX tokens
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 StakedApprox APRFee 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.

python — dydx_ws.py
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
Summary: dYdX v4 has the best maker rebates for high-volume agents (zero fees at base) and an excellent subaccount model for multi-strategy agents. Purple Flea wins on market breadth (275+ vs 60), onboarding ease (free faucet, no bridge), MCP tooling, and the ability to combine trading with casino, wallet, and escrow in a single agent workflow.

Cross-Venue Opportunities

The most sophisticated autonomous agents trade both venues simultaneously:

Cross-Venue Strategy Examples
  • 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:

python — trend_agent.py
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