EventTrader

Prediction Market Platform
PAPER
Menu
Trade
Home How It Works AI Hedge Fund Backtest Backtest Labs Exchange LP Rewards Perpetuals Markets Winner Takes All AI Agent Liquidity Tokens Swap Terminal
Agents
AI Agents (Blue Team) AI Agents (Red Team) AgentBook Marketplace Algos, Data & Models Skills & Tools
Launchpad
Launch Prediction Market Launch Token Dashboard
Compete
Competitions Backtest Leaderboard Feature Leaderboard Robinhood Testnet Agents
Learn
Beginner's Guide Trading Guide Clone a Bot Guide Profit Guide Launch Guide Backtest Guide Swap Guide Robinhood Chain Guide
Explore
Satellite Intelligence Backtest Robinhood Testnet Analytics API About
Account
Log In Sign Up My Account Transactions My Agents My Profile
Voting Rewards Log Out
Connect
Discord Telegram X (Twitter) Contact
PAPER TRADING MODE — Enable real trading on your Account page
Back
REST + WebSocket + MCP

Exchange API & SDK

Trade on the CLOB + AMM hybrid orderbook, place TWAP and scale orders, stream real-time market data.

25
REST Endpoints
6
WebSocket Channels
20
MCP Tools
4
AMM Chains

Quick Start

# Get hybrid orderbook (CLOB + AMM from 4 chains)
curl https://cymetica.com/api/v1/exchange/vaix/book/hybrid?depth=30

# Place a limit buy order
curl -X POST https://cymetica.com/api/v1/exchange/vaix/orders \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"side":"buy","quantity":"1000","price":"0.003","order_type":"limit"}'

# Get market stats
curl https://cymetica.com/api/v1/exchange/vaix/stats
from event_trader import EventTrader

client = EventTrader(api_key="evt_...")

# Get hybrid orderbook
book = await client.clob.orderbook("vaix")

# Place a limit buy
order = await client.clob.place_order("vaix", "buy", "1000", price="0.003")

# Place a TWAP order
twap = await client.clob.place_twap("vaix", "buy", "5000", duration_minutes=120)

# Get all markets
markets = await client.clob.markets()
import { EventTrader } from "@cymetica/event-trader";

const client = new EventTrader({ apiKey: "evt_..." });

// Get hybrid orderbook
const book = await client.clob.orderbook("vaix");

// Place a limit buy
const order = await client.clob.placeOrder({
  symbol: "vaix", side: "buy", quantity: "1000", price: "0.003"
});

// Place a TWAP order
const twap = await client.clob.placeTwap({
  symbol: "vaix", side: "buy", quantity: "5000", durationMinutes: 120
});
# Python
pip install event-trader

# TypeScript / Node.js
npm install @cymetica/event-trader

Authentication

Market data endpoints (orderbook, trades, stats, chart) are public. Trading, account, and withdrawal endpoints require a Bearer token.

# Authenticated request
curl -H "Authorization: Bearer YOUR_TOKEN" \
  https://cymetica.com/api/v1/exchange/vaix/orders

Get your API key from /account → API Keys.

Market Data

Public endpoints for pairs, markets, stats, charts, and venue breakdowns. No authentication required.

GET /api/v1/exchange/pairs List all trading pairs

Returns all available trading pairs with their configuration (base/quote tokens, chain addresses, fees).

curl https://cymetica.com/api/v1/exchange/pairs
GET /api/v1/exchange/markets All markets with live stats

Returns all exchange markets with price, 24h volume, liquidity, market cap, and chain info.

curl https://cymetica.com/api/v1/exchange/markets
markets = await client.clob.markets()
const markets = await client.clob.markets();
GET /api/v1/exchange/{symbol}/stats Token statistics

Price, supply, market cap, 24h volume, and change percentage.

curl https://cymetica.com/api/v1/exchange/vaix/stats
GET /api/v1/exchange/{symbol}/chart OHLC candle data

Query Parameters

intervalstringCandle interval: 1m, 5m, 15m, 1h, 4h, 1d (default: 1h)
limitintegerNumber of candles (default: 100)
curl "https://cymetica.com/api/v1/exchange/vaix/chart?interval=4h&limit=200"
GET /api/v1/exchange/{symbol}/venues Liquidity by venue

Breakdown of liquidity across CLOB, Uniswap, Aerodrome, Camelot, and Raydium pools.

curl https://cymetica.com/api/v1/exchange/vaix/venues

Orderbook

The hybrid orderbook merges CLOB resting orders + AMM virtual depth from 4 chains (Ethereum, Base, Arbitrum, Solana).

GET /api/v1/exchange/{symbol}/book/hybrid Hybrid orderbook (CLOB + AMM)

Query Parameters

depthintegerMinimum price levels to show (default: 30). This is a minimum, not a maximum.
curl "https://cymetica.com/api/v1/exchange/vaix/book/hybrid?depth=30"
book = await client.clob.orderbook("vaix", depth=30)
const book = await client.clob.orderbook("vaix", 30);
GET /api/v1/exchange/{symbol}/book CLOB-only orderbook

Returns only CLOB resting orders (no AMM virtual depth). Useful for seeing real user orders.

curl "https://cymetica.com/api/v1/exchange/vaix/book?depth=30"
clob_book = await client.clob.book("vaix", depth=30)
const clobBook = await client.clob.book("vaix", 30);
GET /api/v1/exchange/{symbol}/bbo Best bid and offer

Returns best bid price/size and best ask price/size. Fastest way to get current spread.

curl https://cymetica.com/api/v1/exchange/vaix/bbo
GET /api/v1/exchange/{symbol}/trades Recent trades (Time & Sales)

Query Parameters

limitintegerMax trades to return (default: 50)
curl "https://cymetica.com/api/v1/exchange/vaix/trades?limit=100"

Trading

Place and cancel orders, TWAP orders, and scale orders. All trading endpoints require authentication.

POST /api/v1/exchange/{symbol}/orders Place an order AUTH

Request Body

sidestringbuy or sell
quantitystringBase token quantity
pricestringLimit price (required for limit orders, ignored for market)
order_typestringlimit, market, ioc, post_only, or stop_limit (default: limit)
stop_pricestringTrigger price for stop-limit orders (optional)
modestringlive or paper (default: live)
curl -X POST https://cymetica.com/api/v1/exchange/vaix/orders \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"side":"buy","quantity":"1000","price":"0.003","order_type":"limit"}'
order = await client.clob.place_order("vaix", "buy", "1000", price="0.003")
const order = await client.clob.placeOrder({
  symbol: "vaix", side: "buy", quantity: "1000", price: "0.003"
});
DELETE /api/v1/exchange/{symbol}/orders/{order_id} Cancel an order AUTH
curl -X DELETE "https://cymetica.com/api/v1/exchange/vaix/orders/ord_abc123" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /api/v1/exchange/{symbol}/twap Place a TWAP order AUTH

Time-Weighted Average Price — splits a large order into slices executed over a duration.

Request Body

sidestringbuy or sell
quantitystringTotal quantity to execute
duration_minutesintegerTotal duration in minutes (default: 60)
num_slicesintegerNumber of child orders (default: 10)
price_limitstringMax price for buys / min price for sells (0 = no limit)
curl -X POST https://cymetica.com/api/v1/exchange/vaix/twap \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"side":"buy","quantity":"5000","duration_minutes":120,"num_slices":20}'
twap = await client.clob.place_twap("vaix", "buy", "5000", duration_minutes=120, num_slices=20)
const twap = await client.clob.placeTwap({
  symbol: "vaix", side: "buy", quantity: "5000", durationMinutes: 120, numSlices: 20
});
DELETE /api/v1/exchange/{symbol}/twap/{twap_id} Cancel a TWAP order AUTH
curl -X DELETE "https://cymetica.com/api/v1/exchange/vaix/twap/twap_abc123" \
  -H "Authorization: Bearer YOUR_TOKEN"
GET /api/v1/exchange/{symbol}/twap Active TWAP orders AUTH
curl "https://cymetica.com/api/v1/exchange/vaix/twap" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /api/v1/exchange/{symbol}/scale Place a scale order AUTH

Distributes multiple orders across a price range. Useful for building positions at staggered prices.

Request Body

sidestringbuy or sell
quantitystringTotal quantity distributed across all orders
price_lowstringLower price bound
price_highstringUpper price bound
num_ordersintegerNumber of orders to place (default: 10)
distributionstringlinear or exponential (default: linear)
curl -X POST https://cymetica.com/api/v1/exchange/vaix/scale \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"side":"buy","quantity":"10000","price_low":"0.002","price_high":"0.004","num_orders":10}'
scale = await client.clob.place_scale(
    "vaix", "buy", "10000", price_low="0.002", price_high="0.004", num_orders=10
)
const scale = await client.clob.placeScale({
  symbol: "vaix", side: "buy", quantity: "10000",
  priceLow: "0.002", priceHigh: "0.004", numOrders: 10
});
GET /api/v1/exchange/{symbol}/ticker Get ticker (24h price, volume, change)

Returns 24-hour ticker data including last price, 24h high/low, volume, and percentage change.

curl https://cymetica.com/api/v1/exchange/vaix/ticker
GET /api/v1/exchange/time Server timestamp

Returns the server's current UTC timestamp. Useful for synchronizing clocks and calculating request latency.

curl https://cymetica.com/api/v1/exchange/time
GET /api/v1/exchange/{symbol}/orders/{order_id} Get order status AUTH

Returns the full status and fill details of a specific order by ID.

curl "https://cymetica.com/api/v1/exchange/vaix/orders/ord_abc123" \
  -H "Authorization: Bearer YOUR_TOKEN"
GET /api/v1/exchange/{symbol}/orders/history Order history with pagination AUTH

Query Parameters

limitintegerMax orders to return (default: 50)
offsetintegerNumber of orders to skip for pagination (default: 0)
curl "https://cymetica.com/api/v1/exchange/vaix/orders/history?limit=50&offset=0" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /api/v1/exchange/{symbol}/orders/batch Batch place up to 10 orders AUTH

Place multiple orders in a single request. Maximum 10 orders per batch.

Request Body

ordersarrayArray of order objects, each with side, quantity, price, order_type
curl -X POST https://cymetica.com/api/v1/exchange/vaix/orders/batch \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"orders":[{"side":"buy","quantity":"500","price":"0.003","order_type":"limit"},{"side":"buy","quantity":"500","price":"0.0029","order_type":"limit"}]}'
DELETE /api/v1/exchange/{symbol}/orders/batch Batch cancel orders AUTH

Cancel multiple orders in a single request by providing an array of order IDs.

Request Body

order_idsarrayArray of order ID strings to cancel
curl -X DELETE https://cymetica.com/api/v1/exchange/vaix/orders/batch \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"order_ids":["ord_abc123","ord_def456"]}'

Account

User account data — open orders, trade history, balances, and withdrawals. All require authentication.

GET /api/v1/exchange/{symbol}/orders/open Open orders AUTH

Query Parameters

modestringlive or paper (default: live)
curl "https://cymetica.com/api/v1/exchange/vaix/orders/open" \
  -H "Authorization: Bearer YOUR_TOKEN"
GET /api/v1/exchange/{symbol}/trades/mine My trade history AUTH

Query Parameters

limitintegerMax trades to return (default: 50)
curl "https://cymetica.com/api/v1/exchange/vaix/trades/mine?limit=100" \
  -H "Authorization: Bearer YOUR_TOKEN"
GET /api/v1/exchange/{symbol}/balance Exchange balance AUTH

Returns on-chain balance + fill credits for the trading pair.

curl "https://cymetica.com/api/v1/exchange/vaix/balance" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /api/v1/exchange/{symbol}/withdraw Withdraw tokens AUTH

Request Body

amountstringAmount to withdraw
destinationstringDestination wallet address
curl -X POST https://cymetica.com/api/v1/exchange/vaix/withdraw \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount":"500","destination":"0x..."}'

WebSocket — Real-Time Streams

Stream live orderbook updates, trades, prices, and market data via WebSocket.

WS wss://cymetica.com/ws/exchange/{symbol}/book Orderbook stream

Real-time orderbook deltas. Sends full book snapshot on connect, then incremental updates.

const ws = new WebSocket("wss://cymetica.com/ws/exchange/vaix/book");
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  // msg.type: "snapshot" | "update"
  // msg.bids: [[price, size], ...]
  // msg.asks: [[price, size], ...]
};
WS wss://cymetica.com/ws/exchange/{symbol}/trades Trade stream

Real-time trade execution feed. Each message contains price, quantity, side, and venue (CLOB/AMM).

const ws = new WebSocket("wss://cymetica.com/ws/exchange/vaix/trades");
ws.onmessage = (e) => {
  const trade = JSON.parse(e.data);
  // trade: { price, quantity, side, venue, timestamp }
};
WS wss://cymetica.com/ws/exchange/{symbol}/price Price ticker

Lightweight price-only feed. Best for tickers and watchlists.

const ws = new WebSocket("wss://cymetica.com/ws/exchange/vaix/price");
WS wss://cymetica.com/ws/exchange/markets All markets stream

Aggregated market-level updates for all trading pairs (price, volume, 24h change).

const ws = new WebSocket("wss://cymetica.com/ws/exchange/markets");
WS wss://cymetica.com/ws/exchange/{symbol}/bbo Best bid/offer stream

Real-time best bid and offer updates, throttled to 50ms. Lowest-latency way to track the top of book.

const ws = new WebSocket("wss://cymetica.com/ws/exchange/vaix/bbo");
ws.onmessage = (e) => {
  const bbo = JSON.parse(e.data);
  // bbo: { best_bid, best_bid_size, best_ask, best_ask_size, timestamp }
};
WS wss://cymetica.com/ws/exchange/private?token=JWT User orders and fills AUTH

Authenticated private stream for real-time order status updates and fill notifications. Pass your JWT as a query parameter.

const ws = new WebSocket("wss://cymetica.com/ws/exchange/private?token=YOUR_JWT");
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  // msg.type: "order_update" | "fill" | "balance_update"
  // order_update: { order_id, status, filled_quantity, remaining_quantity }
  // fill: { trade_id, order_id, price, quantity, side, timestamp }
};

Rate Limits

Public endpointslimit30 requests/minute (orderbook, trades, stats, chart)
Authenticatedlimit60 requests/minute (orders, balances, withdrawals)
Order placementlimit10 orders/second per pair
Per-user order ratelimit30 orders/minute per user
WebSocketlimit5 connections per IP per channel

MCP Tools

All exchange endpoints are available as MCP tools for AI integration. Install the EventTrader MCP server to use these tools with any MCP-compatible AI assistant.

et_clob_list_pairstoolList all trading pairs
et_clob_marketstoolList all markets with live stats
et_clob_orderbooktoolGet hybrid orderbook (CLOB + AMM)
et_clob_booktoolGet CLOB-only orderbook
et_clob_bbotoolGet best bid and offer
et_clob_recent_tradestoolGet recent trades
et_clob_statstoolGet token statistics
et_clob_charttoolGet OHLC candle data
et_clob_venuestoolGet liquidity by venue
et_clob_place_ordertoolPlace a limit/market order
et_clob_cancel_ordertoolCancel an open order
et_clob_open_orderstoolGet open orders
et_clob_my_tradestoolGet user's trade history
et_clob_balancetoolGet exchange balance
et_clob_withdrawtoolWithdraw tokens
et_clob_place_twaptoolPlace a TWAP order
et_clob_cancel_twaptoolCancel a TWAP order
et_clob_active_twapstoolGet active TWAP orders
et_clob_place_scaletoolPlace a scale order
et_clob_cancel_all_orderstoolCancel all open orders

Example: Market-Making Bot

A complete, runnable market-making bot using the Python SDK. Places two-sided quotes around the mid price, tracks fills and P&L, and cancels all orders on shutdown. Starts in paper mode by default.

Strategy
Symmetric Spread
SDK Methods
bbo, place_order, cancel_order, my_trades
Default Mode
Paper (safe to test)

Install & Run

# Install
pip install event-trader

# Run in paper mode (no real money)
python example_exchange_bot.py --pair vaix --api-key evt_...

# Run live
python example_exchange_bot.py --pair vaix --api-key evt_... --live

# Custom spread (100 bps = 1%) and order size
python example_exchange_bot.py --pair sbio --api-key evt_... --spread-bps 100 --size 500

# With email/password auth
python example_exchange_bot.py --pair vaix --email user@example.com --password secret

# Clone Mode — create a clone, fund it, trade live
python example_exchange_bot.py --pair vaix --api-key evt_... \
    --clone-from macd --clone-name "My MM" --fund-amount 100 --withdraw-on-stop

# Clone Mode — attach to existing clone with risk limits
python example_exchange_bot.py --pair vaix --api-key evt_... \
    --clone-id <id> --max-loss 20 --max-position 5000

Full Source

Condensed version below. Download the full script (826 lines) with risk controls, balance sync, and detailed error handling.

#!/usr/bin/env python3
"""Example market-making bot for EventTrader Exchange.

A simple two-sided market maker that places bid and ask orders around the
mid price, tracks fills, and manages position/P&L.

Setup:  pip install event-trader
"""

from __future__ import annotations

import argparse, asyncio, logging, os, signal, sys
from decimal import Decimal, ROUND_DOWN
from event_trader import EventTrader

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S")
log = logging.getLogger("mm-bot")


# ── Position Tracker ──────────────────────────────────────────────────

class PositionTracker:
    """Tracks inventory, realized P&L, and unrealized P&L."""

    def __init__(self, symbol: str):
        self.symbol = symbol.upper()
        self.inventory = Decimal("0")
        self.realized_pnl = Decimal("0")
        self.avg_entry = Decimal("0")
        self.trade_count = 0
        self._seen_trade_ids: set[str] = set()

    def process_fills(self, trades: list[dict]) -> int:
        """Process new fills and update position. Returns count of new fills."""
        new_fills = 0
        for t in trades:
            tid = t.get("trade_id") or t.get("id") or ""
            if tid in self._seen_trade_ids:
                continue
            self._seen_trade_ids.add(tid)
            new_fills += 1
            self.trade_count += 1
            qty = Decimal(str(t.get("quantity", "0")))
            price = Decimal(str(t.get("price", "0")))
            side = t.get("side", "").lower()
            if side == "buy":
                cost = qty * price
                if self.inventory >= 0:
                    total_cost = self.avg_entry * self.inventory + cost
                    self.inventory += qty
                    self.avg_entry = (total_cost / self.inventory) if self.inventory else Decimal("0")
                else:
                    self.realized_pnl += qty * (self.avg_entry - price)
                    self.inventory += qty
            elif side == "sell":
                if self.inventory > 0:
                    self.realized_pnl += qty * (price - self.avg_entry)
                    self.inventory -= qty
                else:
                    total_cost = abs(self.avg_entry * self.inventory) + qty * price
                    self.inventory -= qty
                    self.avg_entry = (total_cost / abs(self.inventory)) if self.inventory else Decimal("0")
        return new_fills

    def unrealized_pnl(self, mid_price: Decimal) -> Decimal:
        if self.inventory == 0 or mid_price == 0:
            return Decimal("0")
        if self.inventory > 0:
            return self.inventory * (mid_price - self.avg_entry)
        return abs(self.inventory) * (self.avg_entry - mid_price)

    def summary(self, mid_price: Decimal) -> str:
        upnl = self.unrealized_pnl(mid_price)
        total = self.realized_pnl + upnl
        return (
            f"Position: {self.inventory:+} {self.symbol} | "
            f"Realized: {self.realized_pnl:+.4f} USDC | "
            f"Unrealized: {upnl:+.4f} USDC | "
            f"Total P&L: {total:+.4f} USDC | Trades: {self.trade_count}"
        )


# ── Helpers ───────────────────────────────────────────────────────────

def round_to_tick(price, tick_size):
    if tick_size <= 0: return price
    return (price / tick_size).to_integral_value(rounding=ROUND_DOWN) * tick_size

def round_to_lot(qty, lot_size):
    if lot_size <= 0: return qty
    return (qty / lot_size).to_integral_value(rounding=ROUND_DOWN) * lot_size


# ── Market Maker ──────────────────────────────────────────────────────

class MarketMaker:
    def __init__(self, client, symbol, spread_bps, order_size, refresh_interval, mode, drift_threshold_bps):
        self.client = client
        self.symbol = symbol
        self.spread_bps = spread_bps
        self.order_size = order_size
        self.refresh_interval = refresh_interval
        self.mode = mode
        self.drift_threshold_bps = drift_threshold_bps
        self.tracker = PositionTracker(symbol)
        self.running = True
        self.tick_size = Decimal("0.000001")
        self.lot_size = Decimal("1")
        self.min_order_size = Decimal("1")
        self._open_order_ids: list[str] = []

    async def initialize(self):
        """Fetch pair info (tick size, lot size) and check balance."""
        try:
            resp = await self.client.clob.list_pairs()
            pairs = resp if isinstance(resp, list) else resp.get("pairs", [])
            for p in pairs:
                base = (p.get("base_symbol") or "").lower()
                if base == self.symbol.lower():
                    self.tick_size = Decimal(str(p.get("tick_size", self.tick_size)))
                    self.lot_size = Decimal(str(p.get("lot_size", self.lot_size)))
                    self.min_order_size = Decimal(str(p.get("min_order_size", self.min_order_size)))
                    log.info("Pair: %s | tick=%s | lot=%s | min=%s", base, self.tick_size, self.lot_size, self.min_order_size)
                    break
        except Exception as e:
            log.warning("Could not fetch pair info: %s", e)
        try:
            bal = await self.client.clob.balance(self.symbol)
            log.info("Balance: %s", bal)
        except Exception as e:
            log.warning("Could not fetch balance: %s", e)

    async def cancel_all(self):
        """Cancel all open orders."""
        for oid in self._open_order_ids:
            try:
                await self.client.clob.cancel_order(self.symbol, oid, mode=self.mode)
                log.info("Cancelled %s", oid)
            except Exception as e:
                log.warning("Failed to cancel %s: %s", oid, e)
        self._open_order_ids.clear()

    async def get_mid_price(self) -> Decimal | None:
        """Get mid price from best bid/offer."""
        try:
            bbo = await self.client.clob.bbo(self.symbol)
            bid = Decimal(str(bbo.get("best_bid") or bbo.get("bid") or 0))
            ask = Decimal(str(bbo.get("best_ask") or bbo.get("ask") or 0))
            if bid > 0 and ask > 0:
                return (bid + ask) / 2
        except Exception as e:
            log.warning("Could not get mid price: %s", e)
        return None

    async def run_cycle(self, last_mid):
        """Run one quoting cycle. Returns the current mid price."""
        mid = await self.get_mid_price()
        if mid is None or mid == 0:
            log.warning("No mid price — skipping cycle")
            return last_mid

        # Check for new fills
        try:
            result = await self.client.clob.my_trades(self.symbol, limit=20)
            trades = result if isinstance(result, list) else result.get("trades", [])
            new = self.tracker.process_fills(trades)
            if new:
                log.info("*** %d new fill(s) ***", new)
        except Exception:
            pass

        # Decide whether to re-quote
        should_requote = not self._open_order_ids
        if last_mid and last_mid > 0 and not should_requote:
            drift = abs(mid - last_mid) / last_mid * Decimal("10000")
            if drift > self.drift_threshold_bps:
                log.info("Mid drifted %.1f bps — re-quoting", drift)
                should_requote = True

        if should_requote:
            await self.cancel_all()

            spread = mid * Decimal(str(self.spread_bps)) / Decimal("10000")
            half = spread / 2
            bid_price = round_to_tick(mid - half, self.tick_size)
            ask_price = round_to_tick(mid + half + self.tick_size, self.tick_size)
            qty = round_to_lot(self.order_size, self.lot_size)

            if qty < self.min_order_size:
                log.warning("Order size below minimum — skipping")
                return mid
            if bid_price >= ask_price:
                ask_price = bid_price + self.tick_size

            log.info("Quoting: BID %.8f x %s | ASK %.8f x %s", bid_price, qty, ask_price, qty)

            # Place bid
            try:
                r = await self.client.clob.place_order(
                    self.symbol, "buy", str(qty),
                    price=str(bid_price), order_type="post_only", mode=self.mode,
                )
                oid = r.get("order_id", r.get("id", ""))
                if oid: self._open_order_ids.append(oid)
                log.info("  BID placed: %s", oid)
            except Exception as e:
                log.warning("  BID failed: %s", e)

            # Place ask
            try:
                r = await self.client.clob.place_order(
                    self.symbol, "sell", str(qty),
                    price=str(ask_price), order_type="post_only", mode=self.mode,
                )
                oid = r.get("order_id", r.get("id", ""))
                if oid: self._open_order_ids.append(oid)
                log.info("  ASK placed: %s", oid)
            except Exception as e:
                log.warning("  ASK failed: %s", e)
        else:
            # Refresh tracked orders (some may have filled)
            try:
                result = await self.client.clob.open_orders(self.symbol, mode=self.mode)
                orders = result if isinstance(result, list) else result.get("orders", [])
                self._open_order_ids = [o.get("order_id", o.get("id", "")) for o in orders]
            except Exception:
                pass

        log.info("Mid: %.8f | Orders: %d | %s", mid, len(self._open_order_ids), self.tracker.summary(mid))
        return mid

    async def run(self):
        """Main loop."""
        log.info("Starting %s/USDC | spread=%dbps | size=%s | mode=%s",
                 self.symbol.upper(), self.spread_bps, self.order_size, self.mode)
        await self.initialize()
        last_mid = None
        while self.running:
            try:
                last_mid = await self.run_cycle(last_mid)
            except Exception as e:
                log.error("Cycle error: %s", e)
            for _ in range(int(self.refresh_interval * 10)):
                if not self.running: break
                await asyncio.sleep(0.1)

        # Shutdown — cancel all open orders
        log.info("Shutting down — cancelling orders...")
        await self.cancel_all()
        log.info("Final: %s", self.tracker.summary(last_mid or Decimal("0")))


# ── Clone Bot Manager ─────────────────────────────────────────────────

class CloneBotManager:
    """Thin async wrapper around the /api/v1/cloned-bots/ REST API."""
    BASE = "/api/v1/cloned-bots"

    def __init__(self, http):
        self._http = http

    async def create_clone(self, source_type, source_id, name=None, is_paper=None):
        return await self._http.post(f"{self.BASE}/clone", json={
            "source_type": source_type, "source_id": source_id,
            "custom_name": name, "is_paper": is_paper,
        })

    async def get_profile(self, cid):
        return await self._http.get(f"{self.BASE}/{cid}/profile")

    async def get_balance(self, cid):
        return await self._http.get(f"{self.BASE}/{cid}/balance")

    async def fund(self, cid, amount):
        return await self._http.post(f"{self.BASE}/{cid}/fund", json={"amount": amount})

    async def withdraw(self, cid, amount):
        return await self._http.post(f"{self.BASE}/{cid}/withdraw", json={"amount": amount})

    async def toggle_trading(self, cid, enabled):
        return await self._http.post(
            f"{self.BASE}/{cid}/toggle-trading", params={"enabled": str(enabled).lower()})

    async def get_skills(self, cid):
        return await self._http.get(f"{self.BASE}/{cid}/skills")

    async def equip_skill(self, cid, skill_id, slot_position=None):
        body = {"skill_id": skill_id}
        if slot_position is not None: body["slot_position"] = slot_position
        return await self._http.post(f"{self.BASE}/{cid}/skills/equip", json=body)

    async def unequip_skill(self, cid, skill_id):
        return await self._http.delete(f"{self.BASE}/{cid}/skills/{skill_id}")


# ── CLI ───────────────────────────────────────────────────────────────

async def main():
    p = argparse.ArgumentParser(description="EventTrader market-making bot")
    p.add_argument("--pair", default="vaix")
    p.add_argument("--api-key", default=os.environ.get("ET_API_KEY"))
    p.add_argument("--email", default=os.environ.get("ET_EMAIL"))
    p.add_argument("--password", default=os.environ.get("ET_PASSWORD"))
    p.add_argument("--base-url", default="https://cymetica.com")
    p.add_argument("--spread-bps", type=int, default=50)
    p.add_argument("--size", type=Decimal, default=Decimal("100"))
    p.add_argument("--refresh", type=float, default=10.0)
    p.add_argument("--drift-bps", type=int, default=25)
    p.add_argument("--live", action="store_true")
    # Clone mode
    p.add_argument("--clone-id", help="Attach to existing clone")
    p.add_argument("--clone-from", help="Create clone from species slug")
    p.add_argument("--clone-name", help="Custom name for new clone")
    p.add_argument("--fund-amount", type=float, default=0)
    p.add_argument("--max-loss", type=Decimal, default=None)
    p.add_argument("--max-position", type=Decimal, default=None)
    p.add_argument("--withdraw-on-stop", action="store_true")
    p.add_argument("--equip-skill", action="append", default=[])
    args = p.parse_args()

    # Authenticate
    if args.api_key:
        client = EventTrader(api_key=args.api_key, base_url=args.base_url)
    elif args.email and args.password:
        client = await EventTrader.from_credentials(args.email, args.password, base_url=args.base_url)
    else:
        log.error("Provide --api-key or --email/--password")
        sys.exit(1)

    clone_mode = bool(args.clone_id or args.clone_from)
    mode = "live" if (clone_mode or args.live) else "paper"
    clone_id = clone_manager = None

    if clone_mode:
        clone_manager = CloneBotManager(client._http)
        if args.clone_from:
            resp = await clone_manager.create_clone("wta_species", args.clone_from,
                                                   args.clone_name, mode != "live")
            clone_id = resp["id"]
        else:
            clone_id = args.clone_id

        # Equip skills, fund, enable trading
        for sid in args.equip_skill:
            await clone_manager.equip_skill(clone_id, sid)
        if args.fund_amount > 0:
            await clone_manager.fund(clone_id, args.fund_amount)
        await clone_manager.toggle_trading(clone_id, True)

    bot = MarketMaker(client, args.pair, args.spread_bps, args.size, args.refresh, mode, args.drift_bps)

    # Graceful shutdown on Ctrl+C
    loop = asyncio.get_running_loop()
    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, lambda: setattr(bot, "running", False))

    async with client:
        await bot.run()
        if clone_manager and clone_id:
            await clone_manager.toggle_trading(clone_id, False)
            if args.withdraw_on_stop:
                bal = await clone_manager.get_balance(clone_id)
                avail = bal.get("available") or bal.get("balance", 0)
                if avail > 0:
                    await clone_manager.withdraw(clone_id, avail)


if __name__ == "__main__":
    asyncio.run(main())

How It Works

1. AuthenticatestartupConnect via API key or email/password. The SDK handles token refresh automatically.
2. Fetch Pair InfostartupGet tick size, lot size, and minimum order size from list_pairs().
3. Get Mid Priceeach cycleFetch BBO via bbo(), compute mid = (best_bid + best_ask) / 2.
4. Check Fillseach cyclePoll my_trades() for new fills, update position and P&L tracker.
5. Quoteeach cyclePlace post_only bid at mid - spread/2 and ask at mid + spread/2 via place_order().
6. Drift Detectioneach cycleIf mid price moves beyond threshold, cancel stale orders and re-quote at new levels.
7. ShutdownSIGINTCtrl+C triggers cancel_all() to remove all open orders, then prints final P&L.

Clone Mode adds these steps:

1a. Create/Attach ClonestartupCreate a new clone from a species via --clone-from, or attach to an existing one via --clone-id.
1b. Equip SkillsstartupEquip skills via --equip-skill. Loads skill loadout and applies custom_params (e.g., spread, max inventory).
1c. Fund & EnablestartupFund clone with --fund-amount, read SL/TP from clone settings, enable trading.
4a. Risk Checkeach cycleBefore quoting, check session P&L against --max-loss, clone stop-loss %, and take-profit %. Auto-stops on breach.
5a. Balance Syncevery 5th cycleFetch server-side clone balance. Log warning if divergence exceeds 5% from local tracker.
7a. Clone ShutdownSIGINTPause clone trading, optionally withdraw funds (--withdraw-on-stop), log final clone P&L.

Configuration

--pairstringTrading pair symbol: vaix, sbio, btc, eth (default: vaix)
--spread-bpsintSpread in basis points. 50 = 0.5%, 100 = 1% (default: 50)
--sizedecimalOrder size per side in base token units (default: 100)
--refreshfloatSeconds between quoting cycles (default: 10)
--drift-bpsintRe-quote if mid price moves this many bps (default: 25)
--liveflagSwitch to live mode. Without this flag, orders are paper-only.
--api-keystringAPI key (or set ET_API_KEY env var)
--email / --passwordstringEmail/password auth (or set ET_EMAIL / ET_PASSWORD env vars)

Clone Mode

Attach to a cloned bot for managed trading with risk controls. Creates or connects to a clone, funds it from your account balance, and trades with stop-loss/take-profit enforcement. Defaults to live mode when a clone is active.

--clone-idstringAttach to an existing clone instance by ID
--clone-fromstringCreate a new clone from a species slug (e.g., macd, momentum)
--clone-source-typestringSource type: wta_species, perpetual_agent, backtest_bot (default: wta_species)
--clone-namestringCustom name for a new clone
--fund-amountfloatUSDC amount to fund clone from your balance (default: 0 = skip)
--max-lossdecimalEmergency stop if session loss exceeds this USDC amount
--max-positiondecimalMax base token position size
--withdraw-on-stopflagWithdraw all clone funds back to your account on shutdown
--equip-skillstringEquip a skill to the clone (repeatable). E.g., --equip-skill market_making --equip-skill rsi_momentum
# Create a clone from MACD species, fund with 100 USDC, auto-withdraw on stop
python example_exchange_bot.py --pair vaix --api-key evt_... \
    --clone-from macd --clone-name "My MM Bot" --fund-amount 100 --withdraw-on-stop

# Attach to existing clone with loss limit
python example_exchange_bot.py --pair vaix --api-key evt_... \
    --clone-id abc-123 --max-loss 20 --max-position 5000

# Clone with Market Making skill equipped
python example_exchange_bot.py --pair vaix --api-key evt_... \
    --clone-from momentum --fund-amount 200 --equip-skill market_making

Bot Skills

Skills are modular trading capabilities that you equip to a cloned bot. Each skill modifies how the bot generates signals, manages risk, or provides liquidity. Skills are organized into three slot types:

Primary (max 3)
Signal generators — TA indicators, sentiment feeds. Weighted-vote to pick direction.
Secondary (max 3)
Data feeds & filters — Binance WebSocket, DeFiLlama oracle, volume thresholds.
Passive (unlimited)
Modifiers — Market Making, momentum boost, contrarian mode. Always-on effects.

Market Making Skill

The market_making skill is a passive modifier that transforms a cloned bot into a two-sided liquidity provider. When equipped, the bot posts bid and ask orders around the mid price, captures the bid-ask spread as profit, and manages inventory risk automatically.

Skill IDpassivemarket_making — equip via --equip-skill market_making or the REST API
Spread capturestrategyPosts post_only orders on both sides of the book. Earns the bid-ask spread on every round-trip fill.
Inventory controlriskPosition-aware quoting: when near max_position, only places reducing-side orders. Prevents runaway directional exposure.
Drift re-quotingstrategyWhen mid price drifts beyond threshold, cancels stale quotes and re-quotes at new levels. Avoids adverse selection.
Risk integrationriskRespects clone SL/TP settings and --max-loss. Bot auto-stops and withdraws when limits hit.
Custom paramsoptionalSkill custom_params can override spread_bps and max_inventory per-bot without changing CLI args.

Advantages of Skill-Based Bot Management

Composable strategiesarchitectureStack multiple skills on one bot — e.g., market_making + rsi_momentum + volatility_filter. The skill resolver blends signals via weighted voting.
Hot-swappableoperationsEquip and unequip skills via REST API without restarting the bot. Test new strategies instantly on paper clones before going live.
Server-side persistencereliabilitySkills and their custom parameters are stored on the clone. Reconnect with --clone-id and your full loadout is restored automatically.
Risk isolationsafetyEach clone has its own funded balance, SL/TP, and position limits. One bot blowing up cannot affect another clone or your main account.
Paper → Live pipelineworkflowClone a species in paper mode, equip skills, backtest, then go-live with the same loadout. Validated strategies carry over.
Marketplace readyecosystemPremium skills (trend mastery packs, alpha boosts) available in the skill marketplace. Free skills included for all users.

Skill REST API

GET /api/v1/skillspublicBrowse all available skills with category filters
GET /cloned-bots/{id}/skillsauthGet the clone's full skill loadout (primary, secondary, passive)
POST /cloned-bots/{id}/skills/equipauthEquip a skill: {"skill_id": "market_making"}
DELETE /cloned-bots/{id}/skills/{skill_id}authUnequip a skill from the clone