Exchange API & SDK
Trade on the CLOB + AMM hybrid orderbook, place TWAP and scale orders, stream real-time market data.
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_...")
# Low-latency preflight: balance + BBO in one call (~5ms)
pf = await client.clob.preflight("vaix")
# Place a limit buy (server-side: ~50ms)
order = await client.clob.place_order("vaix", "buy", "1000", price="0.003")
# Batch place orders (single HTTP call for up to 20 orders)
batch = await client.clob.batch_place_orders("vaix", [
{"side": "buy", "quantity": "500", "price": "0.003", "order_type": "post_only"},
{"side": "sell", "quantity": "500", "price": "0.004", "order_type": "post_only"},
])
# Get all markets
markets = await client.clob.markets()
import { EventTrader } from "@cymetica/event-trader";
const client = new EventTrader({ apiKey: "evt_..." });
// Low-latency preflight: balance + BBO in one call (~5ms)
const pf = await client.clob.preflight("vaix");
// Place a limit buy (server-side: ~50ms)
const order = await client.clob.placeOrder({
symbol: "vaix", side: "buy", quantity: "1000", price: "0.003"
});
// Batch place orders (single HTTP call for up to 20 orders)
const batch = await client.clob.batchPlaceOrders("vaix", [
{ side: "buy", quantity: "500", price: "0.003", orderType: "post_only" },
{ side: "sell", quantity: "500", price: "0.004", orderType: "post_only" },
]);
# 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.
Returns all available trading pairs with their configuration (base/quote tokens, chain addresses, fees).
curl https://cymetica.com/api/v1/exchange/pairs
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();
Price, supply, market cap, 24h volume, and change percentage.
curl https://cymetica.com/api/v1/exchange/vaix/stats
Query Parameters
1m, 5m, 15m, 1h, 4h, 1d (default: 1h)curl "https://cymetica.com/api/v1/exchange/vaix/chart?interval=4h&limit=200"
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).
Query Parameters
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);
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);
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
Query Parameters
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.
Request Body
buy or selllimit, post_only, or stop_limit (default: limit)live 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"
});
curl -X DELETE "https://cymetica.com/api/v1/exchange/vaix/orders/ord_abc123" \
-H "Authorization: Bearer YOUR_TOKEN"
Time-Weighted Average Price — splits a large order into slices executed over a duration.
Request Body
buy or sellcurl -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
});
curl -X DELETE "https://cymetica.com/api/v1/exchange/vaix/twap/twap_abc123" \
-H "Authorization: Bearer YOUR_TOKEN"
curl "https://cymetica.com/api/v1/exchange/vaix/twap" \
-H "Authorization: Bearer YOUR_TOKEN"
Distributes multiple orders across a price range. Useful for building positions at staggered prices.
Request Body
buy or selllinear 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
});
Returns 24-hour ticker data including last price, 24h high/low, volume, and percentage change.
curl https://cymetica.com/api/v1/exchange/vaix/ticker
Returns the server's current UTC timestamp. Useful for synchronizing clocks and calculating request latency.
curl https://cymetica.com/api/v1/exchange/time
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"
Query Parameters
curl "https://cymetica.com/api/v1/exchange/vaix/orders/history?limit=50&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"
Place multiple orders in a single request. Maximum 10 orders per batch.
Request Body
side, quantity, price, order_typecurl -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"}]}'
Cancel multiple orders in a single request by providing an array of order IDs.
Request Body
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.
Query Parameters
live or paper (default: live)curl "https://cymetica.com/api/v1/exchange/vaix/orders/open" \
-H "Authorization: Bearer YOUR_TOKEN"
Query Parameters
curl "https://cymetica.com/api/v1/exchange/vaix/trades/mine?limit=100" \
-H "Authorization: Bearer YOUR_TOKEN"
Returns on-chain balance + fill credits for the trading pair.
curl "https://cymetica.com/api/v1/exchange/vaix/balance" \
-H "Authorization: Bearer YOUR_TOKEN"
Request Body
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.
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], ...]
};
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 }
};
Lightweight price-only feed. Best for tickers and watchlists.
const ws = new WebSocket("wss://cymetica.com/ws/exchange/vaix/price");
Aggregated market-level updates for all trading pairs (price, volume, 24h change).
const ws = new WebSocket("wss://cymetica.com/ws/exchange/markets");
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 }
};
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
Latency Optimization
The exchange API is optimized for low-latency trading. Server-side order matching completes in <5ms. End-to-end order placement (preflight + POST) achieves <150ms round-trip.
Preflight Endpoint
Returns auth status, ledger balances, and best bid/ask in a single call (~5ms server-side). Replaces separate balance() + bbo() calls which trigger slower on-chain RPC lookups.
Response Fields
SBIO/USDC)curl -H "Authorization: Bearer YOUR_TOKEN" \
https://cymetica.com/api/v1/clob/preflight/vaix
# Response (~5ms server-side):
# {"pair":"VAIX/USDC","base_balance":"50000.00","quote_balance":"125.50",
# "best_bid":"0.003800","best_ask":"0.004200","spread":"0.000400","server_ms":4.2}
# Single call replaces balance() + bbo()
pf = await client.clob.preflight("vaix")
print(f"Balance: {pf['base_balance']} VAIX, {pf['quote_balance']} USDC")
print(f"BBO: {pf['best_bid']}/{pf['best_ask']} ({pf['server_ms']}ms)")
const pf = await client.clob.preflight("vaix");
console.log(`Balance: ${pf.base_balance} VAIX, ${pf.quote_balance} USDC`);
console.log(`BBO: ${pf.best_bid}/${pf.best_ask} (${pf.server_ms}ms)`);
Server Timing
Order responses include a server_timing field with a latency breakdown:
{
"order_id": "abc-123",
"status": "open",
"server_timing": {
"pre_match_ms": 2.5, // Validation + balance check
"match_ms": 2.4, // Matching engine
"commit_ms": 9.7, // Database commit
"total_ms": 45.7 // Total server-side time
}
}
Best Practices for Low-Latency Trading
requests.Session() (Python) or http.Agent (Node)POST /clob/orders/batch to place multiple orders in a single HTTP callpost_only order type for MM quotes — rejects if it would cross the spreadPATCH /clob/orders/{id} to update price/qty atomically/ws/exchange/{symbol}/book instead of polling the REST APIMCP 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.
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.
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
list_pairs().bbo(), compute mid = (best_bid + best_ask) / 2.my_trades() for new fills, update position and P&L tracker.post_only bid at mid - spread/2 and ask at mid + spread/2 via place_order().cancel_all() to remove all open orders, then prints final P&L.Clone Mode adds these steps:
--clone-from, or attach to an existing one via --clone-id.--equip-skill. Loads skill loadout and applies custom_params (e.g., spread, max inventory).--fund-amount, read SL/TP from clone settings, enable trading.--max-loss, clone stop-loss %, and take-profit %. Auto-stops on breach.--withdraw-on-stop), log final clone P&L.Configuration
vaix, sbio, btc, eth (default: vaix)ET_API_KEY env var)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.
macd, momentum)wta_species, perpetual_agent, backtest_bot (default: wta_species)--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:
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.
market_making — equip via --equip-skill market_making or the REST APIpost_only orders on both sides of the book. Earns the bid-ask spread on every round-trip fill.max_position, only places reducing-side orders. Prevents runaway directional exposure.--max-loss. Bot auto-stops and withdraws when limits hit.custom_params can override spread_bps and max_inventory per-bot without changing CLI args.Advantages of Skill-Based Bot Management
market_making + rsi_momentum + volatility_filter. The skill resolver blends signals via weighted voting.--clone-id and your full loadout is restored automatically.go-live with the same loadout. Validated strategies carry over.Skill REST API
{"skill_id": "market_making"}