
Orderly
- 17 installs
- 21 repo stars
- Updated August 3, 2026
- starchild-ai-agent/official-skills
Helps with ai & agent building tasks during AI-assisted development.
About
orderly is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- orderly
- AI & Agent Building
- AI-coding skill
Orderly by the numbers
- 17 all-time installs (skills.sh)
- Ranked #10,886 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/starchild-ai-agent/official-skills --skill orderlyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 3, 2026 |
| Repository | starchild-ai-agent/official-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Orderly Network Trading
Trade perpetual futures on Orderly Network, an omnichain orderbook DEX built on unified liquidity. Supports both cryptocurrency pairs (169+ assets) and RWA commodities (Gold, Silver). Orders are signed using Ed25519 keys auto-provisioned via this agent's EVM wallet (Privy). Trades execute through Orderly's central limit orderbook with settlement on Arbitrum (or other supported EVM chains).
Available Markets
Orderly Network supports two asset categories:
Cryptocurrency Perpetuals (169+ pairs)
Major cryptocurrencies and altcoins tradable as perpetual futures:
- Major assets: BTC, ETH, SOL, AVAX, MATIC, NEAR, OP, ARB
- DeFi tokens: UNI, AAVE, SUSHI, CRV, COMP
- Layer-1/Layer-2: ATOM, DOT, ADA, FTM, MATIC
- Symbol format:
PERP_<TOKEN>_USDC(e.g.,PERP_BTC_USDC,PERP_ETH_USDC) - Shorthand accepted: Use
"BTC"instead of"PERP_BTC_USDC"— auto-expanded by tools
RWA Commodities (Launched Dec 2025)
Real-world asset commodities available for perpetual futures trading:
- Gold:
$XAUorPERP_XAU_USDC— physical gold price tracking - Silver:
$XAGorPERP_XAG_USDC— physical silver price tracking - Leverage: Up to 20x leverage available
- Pricing: Institutional-grade oracle feeds ensure real-world price accuracy
- Not tokenized: Direct on-chain exposure without wrapping or tokenization
Use orderly_futures() to get the complete real-time list of all available instruments.
Prerequisites
Before trading, the wallet policy must be active. Load the wallet-policy skill and propose the standard wildcard policy (deny key export + allow *). This covers all Orderly operations — USDC deposits, vault interactions, account registration, and withdrawal signing.
Available Tools (19)
Public Tools (read-only, no auth)
| Tool | Description |
|---|---|
orderly_system_info | System maintenance status |
orderly_futures | Futures instrument info (tick sizes, lot sizes, max leverage) |
orderly_funding | Funding rates (current + history) |
orderly_volume | Volume statistics (24h volume, open interest) |
orderly_orderbook | Orderbook snapshot (bids/asks with sizes) |
orderly_kline | OHLCV candlestick data |
orderly_market | Market overview (instruments + recent trades) |
orderly_chain_info | Chain and broker configuration |
Private Tools (require Ed25519 auth)
| Tool | Description |
|---|---|
orderly_account | Account info (fees, tier, status) |
orderly_holdings | Asset balances (available, frozen) |
orderly_positions | Open positions (size, entry, PnL) |
orderly_orders | List orders (open/completed/cancelled) |
orderly_trades | Trade/fill history |
orderly_liquidations | Liquidation history |
Trading Tools (require Ed25519 auth)
| Tool | Description |
|---|---|
orderly_order | Create order (LIMIT/MARKET/IOC/FOK/POST_ONLY) |
orderly_modify | Edit existing order (price/quantity) |
orderly_cancel | Cancel order by ID |
orderly_cancel_all | Cancel all orders (optionally per symbol) |
orderly_leverage | Update leverage for a symbol |
---
Tool Usage Examples
Check System Status
orderly_system_info()Check Futures Instruments
orderly_futures() # All instruments (crypto + RWA)
orderly_futures(symbol="BTC") # BTC perp details
orderly_futures(symbol="XAU") # Gold perp details
orderly_futures(symbol="XAG") # Silver perp detailsCheck Orderbook
orderly_orderbook(symbol="BTC")
orderly_orderbook(symbol="ETH", max_level=10)
orderly_orderbook(symbol="XAU") # Gold orderbook
orderly_orderbook(symbol="XAG", max_level=5) # Silver orderbookGet Candles
orderly_kline(symbol="BTC", interval="1h", limit=100)
orderly_kline(symbol="ETH", interval="4h", limit=200)Intervals: 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d, 1w
Check Funding Rates
orderly_funding(symbol="BTC") # Current rate
orderly_funding(symbol="BTC", history=true) # Current + historyCheck Account Info
orderly_account()Check Holdings
orderly_holdings()Check Positions
orderly_positions()Place a Limit Order
orderly_order(symbol="BTC", side="buy", quantity=0.01, price=95000)
orderly_order(symbol="XAU", side="buy", quantity=1, price=2800) # Buy 1 oz Gold at $2,800
orderly_order(symbol="XAG", side="sell", quantity=10, price=32) # Sell 10 oz Silver at $32Places a GTC limit buy for 0.01 BTC at $95,000.
Place a Market Order
orderly_order(symbol="ETH", side="sell", quantity=0.1, order_type="MARKET")Place a Post-Only Order
orderly_order(symbol="BTC", side="buy", quantity=0.01, price=94000, order_type="POST_ONLY")Rejected if it would immediately fill (maker only).
Close a Position
orderly_order(symbol="BTC", side="sell", quantity=0.01, reduce_only=true)Use reduce_only=true to ensure it only closes, never opens a new position.
Cancel an Order
orderly_cancel(symbol="BTC", order_id=12345678)Get order_id from orderly_orders.
Cancel All Orders
orderly_cancel_all() # Cancel everything
orderly_cancel_all(symbol="BTC") # Cancel only BTC ordersModify an Order
orderly_modify(order_id=12345678, symbol="BTC", side="buy", quantity=0.02, price=94500)Set Leverage
orderly_leverage(symbol="BTC", leverage=10)
orderly_leverage(symbol="ETH", leverage=5)Check Order History
orderly_orders() # All open orders
orderly_orders(symbol="BTC", status="INCOMPLETE") # Open BTC orders
orderly_orders(status="COMPLETED", limit=20) # Recent filled ordersCheck Trade History
orderly_trades() # All recent trades
orderly_trades(symbol="BTC", limit=10) # Recent BTC trades---
Common Workflows
Open a Perp Position
1. orderly_market(symbol="BTC") — Check current price and instrument details 2. orderly_leverage(symbol="BTC", leverage=5) — Set desired leverage 3. orderly_order(symbol="BTC", side="buy", quantity=0.01, price=95000) — Place limit order 4. orderly_orders(symbol="BTC", status="INCOMPLETE") — Verify order is live 5. orderly_trades(symbol="BTC") — Check if filled
Close a Perp Position
1. orderly_positions() — See current positions and sizes 2. orderly_order(symbol="BTC", side="sell", quantity=0.01, order_type="MARKET", reduce_only=true) — Close with market order 3. orderly_positions() — Verify position is closed
Market Research
1. orderly_futures() — Scan all available instruments 2. orderly_funding(symbol="BTC", history=true) — Check funding environment 3. orderly_kline(symbol="BTC", interval="4h", limit=168) — 7-day price action 4. orderly_orderbook(symbol="BTC") — Check liquidity depth 5. orderly_volume() — Check aggregate volume stats
Trading RWA Commodities (Gold/Silver)
1. orderly_futures(symbol="XAU") — Check Gold instrument details (min order size, tick size, leverage limits) 2. orderly_market(symbol="XAU") — Get current Gold price and recent trades 3. orderly_kline(symbol="XAU", interval="1h", limit=24) — 24-hour Gold price chart 4. orderly_leverage(symbol="XAU", leverage=10) — Set 10x leverage for Gold trading 5. orderly_order(symbol="XAU", side="buy", quantity=1, price=2800) — Buy 1 oz Gold at $2,800 6. orderly_positions() — Check Gold/Silver positions 7. orderly_order(symbol="XAU", side="sell", quantity=1, order_type="MARKET", reduce_only=true) — Close Gold position
RWA Trading Notes:
- Gold and Silver use same tools as crypto perpetuals
- Quantities in troy ounces (e.g., 1 = 1 oz Gold, 10 = 10 oz Silver)
- Pricing tracks real-world spot prices via institutional oracle feeds
- Up to 20x leverage available
- USDC settlement like all Orderly perps
---
Order Types
| Type | Parameter | Behavior |
|---|---|---|
| Limit (GTC) | order_type="LIMIT" | Rests on book until filled or cancelled |
| Market | order_type="MARKET" | Fills at best available price immediately |
| IOC | order_type="IOC" | Immediate-or-Cancel: fill what's available, cancel rest |
| FOK | order_type="FOK" | Fill-or-Kill: fill entirely or cancel entirely |
| Post-Only | order_type="POST_ONLY" | Rejected if it would cross the spread (maker only) |
Add reduce_only=true to any order type to ensure it only closes existing positions.
---
Symbol Format
Orderly uses structured symbol names. You can use shorthand (just the coin name) and it auto-expands:
| You type | Expands to |
|---|---|
"BTC" | "PERP_BTC_USDC" |
"ETH" | "PERP_ETH_USDC" |
"PERP_BTC_USDC" | passed through as-is |
"SPOT_ETH_USDC" | passed through as-is |
Full symbol format: {PERP|SPOT}_{BASE}_{QUOTE}
---
Risk Management
- Always check positions before trading — know your existing exposure and margin usage
- Set leverage explicitly before opening new positions
- Use reduce_only when closing to avoid accidentally opening the opposite direction
- Monitor funding rates — high positive funding means longs are expensive to hold
- Start with small sizes — check instrument details for minimum order sizes
- Post-only orders save on fees (maker vs taker rates)
- Check trades after market orders — market orders may get partial fills
---
Error Handling
| Error | Cause | Fix |
|---|---|---|
| "Orderly API 400" | Invalid request parameters | Check symbol name, order type, and required fields |
| "Orderly API 401" | Authentication failed | Ed25519 key may have expired — restart triggers re-registration |
| "Orderly error" | Business logic rejection | Check error message for details (insufficient margin, invalid price, etc.) |
| "Not running on Fly" | No wallet access | Wallet signing only works on Fly.io deployment |
| "Orderly account not registered" | Registration not complete | Private/trading calls auto-register on first use |
| "No ethereum wallet found" | Missing Privy wallet | Ensure WALLET_SERVICE_URL is configured and wallet is provisioned |
| "Policy violation" / signing rejected | Wallet policy doesn't allow required methods | Load the wallet-policy skill and propose the standard wildcard policy (deny key export + allow *) |
"""
Orderly Network Extension — Perpetual Futures & Spot Trading on Orderly DEX
Provides 21 tools for trading on Orderly Network:
- 8 public tools: system info, futures, funding, volume, orderbook, kline, market, chain info
- 6 private tools: account, holdings, positions, orders, trades, liquidations
- 5 trading tools: order, modify, cancel, cancel all, leverage
- 2 fund tools: deposit, withdraw
Authentication: Ed25519 keys auto-provisioned via Privy EIP-712 signing on first use.
No API key env vars needed.
Environment Variables:
- WALLET_SERVICE_URL: Privy wallet service URL (required for signing)
- ORDERLY_API_URL: API base URL (default: https://api.orderly.org)
- ORDERLY_BROKER_ID: Broker identifier (default: woofi_pro)
- ORDERLY_CHAIN_ID: Chain ID (default: 42161 / Arbitrum)
Usage:
This extension is auto-loaded by the ExtensionLoader.
"""
import logging
from typing import List
logger = logging.getLogger(__name__)
def register(api) -> List[str]:
"""
Extension entry point — register all Orderly tools.
Args:
api: ExtensionApi instance with registry and config
Returns:
List of registered tool names
"""
registered = []
try:
from .tools import (
# Public tools (8)
OrderlySystemInfoTool,
OrderlyFuturesTool,
OrderlyFundingTool,
OrderlyVolumeTool,
OrderlyOrderbookTool,
OrderlyKlineTool,
OrderlyMarketTool,
OrderlyChainInfoTool,
# Private tools (6)
OrderlyAccountTool,
OrderlyHoldingsTool,
OrderlyPositionsTool,
OrderlyOrdersTool,
OrderlyTradesTool,
OrderlyLiquidationsTool,
# Trading tools (5)
OrderlyOrderTool,
OrderlyModifyTool,
OrderlyCancelTool,
OrderlyCancelAllTool,
OrderlyLeverageTool,
# Fund tools (2)
OrderlyDepositTool,
OrderlyWithdrawTool,
)
# Public tools
api.register_tool(OrderlySystemInfoTool())
api.register_tool(OrderlyFuturesTool())
api.register_tool(OrderlyFundingTool())
api.register_tool(OrderlyVolumeTool())
api.register_tool(OrderlyOrderbookTool())
api.register_tool(OrderlyKlineTool())
api.register_tool(OrderlyMarketTool())
api.register_tool(OrderlyChainInfoTool())
# Private tools
api.register_tool(OrderlyAccountTool())
api.register_tool(OrderlyHoldingsTool())
api.register_tool(OrderlyPositionsTool())
api.register_tool(OrderlyOrdersTool())
api.register_tool(OrderlyTradesTool())
api.register_tool(OrderlyLiquidationsTool())
# Trading tools
api.register_tool(OrderlyOrderTool())
api.register_tool(OrderlyModifyTool())
api.register_tool(OrderlyCancelTool())
api.register_tool(OrderlyCancelAllTool())
api.register_tool(OrderlyLeverageTool())
# Fund tools
api.register_tool(OrderlyDepositTool())
api.register_tool(OrderlyWithdrawTool())
registered = [
# Public (8)
"orderly_system_info",
"orderly_futures",
"orderly_funding",
"orderly_volume",
"orderly_orderbook",
"orderly_kline",
"orderly_market",
"orderly_chain_info",
# Private (6)
"orderly_account",
"orderly_holdings",
"orderly_positions",
"orderly_orders",
"orderly_trades",
"orderly_liquidations",
# Trading (5)
"orderly_order",
"orderly_modify",
"orderly_cancel",
"orderly_cancel_all",
"orderly_leverage",
# Fund (2)
"orderly_deposit",
"orderly_withdraw",
]
logger.info(f"Registered Orderly tools ({len(registered)} tools)")
except Exception as e:
logger.warning(f"Failed to load Orderly tools: {e}")
return registered
# Extension metadata
EXTENSION_INFO = {
"name": "orderly",
"version": "1.0.0",
"description": "Orderly Network DEX trading — perpetual futures and spot",
"tools": [
"orderly_system_info",
"orderly_futures",
"orderly_funding",
"orderly_volume",
"orderly_orderbook",
"orderly_kline",
"orderly_market",
"orderly_chain_info",
"orderly_account",
"orderly_holdings",
"orderly_positions",
"orderly_orders",
"orderly_trades",
"orderly_liquidations",
"orderly_order",
"orderly_modify",
"orderly_cancel",
"orderly_cancel_all",
"orderly_leverage",
"orderly_deposit",
"orderly_withdraw",
],
"env_vars": [
"WALLET_SERVICE_URL",
"ORDERLY_API_URL",
"ORDERLY_BROKER_ID",
"ORDERLY_CHAIN_ID",
],
}
"""
Orderly Network API Client — async HTTP client for public, private, and trading endpoints.
Public endpoints: unauthenticated GET requests for market data.
Private endpoints: Ed25519-signed requests for account data and trading.
Symbol resolution: accepts "BTC" and auto-expands to "PERP_BTC_USDC".
Full symbols (e.g. "PERP_BTC_USDC", "SPOT_ETH_USDC") are also accepted.
"""
import json
import logging
import os
from typing import Any, Dict, List, Optional
import aiohttp
from . import signing
logger = logging.getLogger(__name__)
DEFAULT_API_URL = "https://api.orderly.org"
class OrderlyClient:
"""
Async Orderly Network client.
- Public methods: GET requests (no auth)
- Private methods: Ed25519-signed requests
- Trading methods: Ed25519-signed POST/PUT/DELETE
"""
def __init__(self, api_url: Optional[str] = None):
self.api_url = api_url or os.environ.get(
"ORDERLY_API_URL", DEFAULT_API_URL
)
self._ready = False
# Cached futures info
self._futures_info: Optional[Dict[str, dict]] = None
# ── Internal helpers ─────────────────────────────────────────────────
async def _ensure_ready(self) -> None:
"""Lazy init: register with Orderly on first private API call."""
if not self._ready:
await signing.ensure_registered()
self._ready = True
async def _public_get(self, path: str, params: Optional[dict] = None) -> Any:
"""Unauthenticated GET request to Orderly public API."""
url = f"{self.api_url}{path}"
async with aiohttp.ClientSession() as session:
async with session.get(
url,
params=params,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
if resp.status >= 400:
body = await resp.text()
raise Exception(f"Orderly API {resp.status}: {body}")
data = await resp.json()
if not data.get("success", True):
raise Exception(f"Orderly error: {data.get('message', data)}")
return data.get("data", data)
async def _private_request(
self,
method: str,
path: str,
params: Optional[dict] = None,
body: Optional[dict] = None,
) -> Any:
"""Ed25519-signed request to Orderly private API."""
await self._ensure_ready()
body_str = json.dumps(body) if body else ""
headers = signing.build_auth_headers(method.upper(), path, body_str)
headers["Content-Type"] = "application/json"
url = f"{self.api_url}{path}"
async with aiohttp.ClientSession() as session:
kwargs = {
"headers": headers,
"timeout": aiohttp.ClientTimeout(total=15),
}
if method.upper() == "GET":
kwargs["params"] = params
elif body:
kwargs["data"] = body_str
async with session.request(method.upper(), url, **kwargs) as resp:
if resp.status >= 400:
resp_body = await resp.text()
logger.error(f"Orderly private API error: {method.upper()} {path} → {resp.status}: {resp_body}")
raise Exception(f"Orderly API {resp.status}: {resp_body}")
data = await resp.json()
if not data.get("success", True):
logger.error(f"Orderly business error: {method.upper()} {path} → {data}")
raise Exception(f"Orderly error: {data.get('message', data)}")
return data.get("data", data)
# ── Symbol Resolution ────────────────────────────────────────────────
async def _ensure_futures_info(self) -> None:
"""Cache futures instrument metadata."""
if self._futures_info is not None:
return
data = await self._public_get("/v1/public/futures")
self._futures_info = {}
rows = data if isinstance(data, list) else data.get("rows", data)
if isinstance(rows, list):
for item in rows:
symbol = item.get("symbol", "")
self._futures_info[symbol] = item
def _resolve_symbol(self, coin_or_symbol: str, market_type: str = "perp") -> str:
"""
Resolve a coin name to an Orderly symbol.
Accepts:
- "BTC" → "PERP_BTC_USDC" (perp) or "SPOT_BTC_USDC" (spot)
- "PERP_BTC_USDC" → passed through as-is
- "SPOT_ETH_USDC" → passed through as-is
"""
upper = coin_or_symbol.upper()
if upper.startswith("PERP_") or upper.startswith("SPOT_"):
return upper
if market_type == "spot":
return f"SPOT_{upper}_USDC"
return f"PERP_{upper}_USDC"
# ── Public Methods (no auth) ─────────────────────────────────────────
async def get_system_info(self) -> dict:
"""Get system maintenance status."""
return await self._public_get("/v1/public/info")
async def get_futures(self, symbol: Optional[str] = None) -> Any:
"""Get futures instrument info."""
if symbol:
return await self._public_get(f"/v1/public/futures/{symbol}")
return await self._public_get("/v1/public/futures")
async def get_funding_rate(self, symbol: str) -> dict:
"""Get current funding rate for a symbol."""
return await self._public_get(f"/v1/public/funding_rate/{symbol}")
async def get_funding_rate_history(
self, symbol: str, limit: int = 24, page: int = 1
) -> Any:
"""Get funding rate history."""
return await self._public_get(
f"/v1/public/funding_rate_history",
params={"symbol": symbol, "limit": limit, "page": page},
)
async def get_volume_stats(self) -> dict:
"""Get volume statistics."""
return await self._public_get("/v1/public/volume/stats")
async def get_orderbook(self, symbol: str, max_level: int = 20) -> dict:
"""Get orderbook snapshot."""
return await self._public_get(
f"/v1/orderbook/{symbol}",
params={"max_level": max_level},
)
async def get_kline(
self,
symbol: str,
kline_type: str = "1h",
limit: int = 100,
) -> Any:
"""Get OHLCV candlestick data."""
return await self._public_get(
"/v1/kline",
params={"symbol": symbol, "type": kline_type, "limit": limit},
)
async def get_market_trades(self, symbol: str, limit: int = 50) -> Any:
"""Get recent market trades."""
return await self._public_get(
f"/v1/public/market_trades",
params={"symbol": symbol, "limit": limit},
)
async def get_chain_info(self) -> dict:
"""Get chain and broker configuration."""
return await self._public_get("/v1/public/chain_info")
# ── Private Methods (Ed25519 auth) ───────────────────────────────────
async def get_account_info(self) -> dict:
"""Get account info (fees, tier, etc.)."""
return await self._private_request("GET", "/v1/client/info")
async def get_holdings(self) -> Any:
"""Get asset balances (holdings)."""
return await self._private_request("GET", "/v1/client/holding")
async def get_positions(self) -> Any:
"""Get open positions."""
return await self._private_request("GET", "/v1/positions")
async def get_orders(
self,
symbol: Optional[str] = None,
status: Optional[str] = None,
limit: int = 50,
) -> Any:
"""Get orders (open or historical)."""
params = {"limit": limit}
if symbol:
params["symbol"] = symbol
if status:
params["status"] = status
return await self._private_request("GET", "/v1/orders", params=params)
async def get_trades(
self,
symbol: Optional[str] = None,
limit: int = 50,
) -> Any:
"""Get trade/fill history."""
params = {"limit": limit}
if symbol:
params["symbol"] = symbol
return await self._private_request("GET", "/v1/trades", params=params)
async def get_liquidations(self, limit: int = 50) -> Any:
"""Get liquidation history."""
return await self._private_request(
"GET", "/v1/liquidations", params={"limit": limit}
)
# ── Trading Methods (Ed25519 auth) ───────────────────────────────────
async def create_order(
self,
symbol: str,
side: str,
order_type: str,
order_quantity: Optional[float] = None,
order_price: Optional[float] = None,
reduce_only: bool = False,
visible_quantity: Optional[float] = None,
order_tag: str = "starchild",
) -> dict:
"""
Create an order.
Args:
symbol: Orderly symbol (e.g. "PERP_BTC_USDC")
side: "BUY" or "SELL"
order_type: "LIMIT", "MARKET", "IOC", "FOK", "POST_ONLY"
order_quantity: Size in base asset
order_price: Limit price (required for LIMIT, IOC, FOK, POST_ONLY)
reduce_only: If True, only reduces position
visible_quantity: Iceberg order visible size
order_tag: Tag for order tracking
"""
broker_id = signing._get_broker_id()
body = {
"symbol": symbol,
"side": side.upper(),
"order_type": order_type.upper(),
"broker_id": broker_id,
"order_tag": order_tag,
}
if order_quantity is not None:
body["order_quantity"] = order_quantity
if order_price is not None:
body["order_price"] = order_price
if reduce_only:
body["reduce_only"] = True
if visible_quantity is not None:
body["visible_quantity"] = visible_quantity
return await self._private_request("POST", "/v1/order", body=body)
async def modify_order(
self,
order_id: int,
symbol: str,
side: str,
order_type: str,
order_quantity: Optional[float] = None,
order_price: Optional[float] = None,
) -> dict:
"""Modify an existing order."""
body = {
"order_id": order_id,
"symbol": symbol,
"side": side.upper(),
"order_type": order_type.upper(),
}
if order_quantity is not None:
body["order_quantity"] = order_quantity
if order_price is not None:
body["order_price"] = order_price
return await self._private_request("PUT", "/v1/order", body=body)
async def cancel_order(self, symbol: str, order_id: int) -> dict:
"""Cancel an order by ID."""
return await self._private_request(
"DELETE",
f"/v1/order?symbol={symbol}&order_id={order_id}",
)
async def cancel_all_orders(self, symbol: Optional[str] = None) -> dict:
"""Cancel all orders, optionally filtered by symbol."""
body = {}
if symbol:
body["symbol"] = symbol
return await self._private_request("DELETE", "/v1/orders", body=body or None)
async def update_leverage(self, symbol: str, leverage: int) -> dict:
"""Update leverage for a symbol."""
return await self._private_request(
"POST",
"/v1/client/leverage",
body={"symbol": symbol, "leverage": leverage},
)
# ── Module-level singleton ───────────────────────────────────────────────────
_client: Optional[OrderlyClient] = None
def _get_client() -> OrderlyClient:
global _client
if _client is None:
_client = OrderlyClient()
return _client
"""
Orderly Network Deposit & Withdraw — smart contract deposits and EIP-712 withdrawals.
Deposit flow (on-chain):
1. ensure_registered() → get account_id
2. Query getDepositFee() via eth_call to Arbitrum RPC
3. TX1: ERC-20 approve(vault, amount) via wallet service
4. TX2: vault.deposit(accountId, brokerHash, tokenHash, amount) via wallet service
Withdraw flow (API + EIP-712):
1. ensure_registered() → get account_id
2. GET /v1/withdraw_nonce (Ed25519 signed)
3. Sign EIP-712 Withdraw message via Privy wallet
4. POST /v1/withdraw_request (Ed25519 signed)
No new dependencies — uses eth_utils.keccak (already in requirements) + manual ABI encoding.
"""
import json
import logging
import os
import time
import aiohttp
from eth_utils import keccak
from tools.wallet import _wallet_request, _is_fly_machine
from . import signing
logger = logging.getLogger(__name__)
# ── Constants ────────────────────────────────────────────────────────────────
VAULT_ADDRESS = "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9"
USDC_ADDRESSES = {
42161: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # Arbitrum
1: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", # Ethereum
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # Base
10: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", # Optimism
}
USDC_DECIMALS = 6
DEFAULT_ARBITRUM_RPC = "https://arb1.arbitrum.io/rpc"
BROKER_HASH = keccak(b"woofi_pro") # bytes32
TOKEN_HASH = keccak(b"USDC") # bytes32
# ── ABI Encoding (manual, no web3 dependency) ───────────────────────────────
def _encode_approve(spender: str, amount: int) -> str:
"""Encode ERC-20 approve(address, uint256) calldata."""
# selector: keccak256("approve(address,uint256)")[:4]
selector = keccak(b"approve(address,uint256)")[:4]
# address → left-padded to 32 bytes
addr_bytes = bytes.fromhex(spender.replace("0x", "")).rjust(32, b"\x00")
# uint256 → big-endian 32 bytes
amount_bytes = amount.to_bytes(32, "big")
return "0x" + (selector + addr_bytes + amount_bytes).hex()
def _encode_deposit(account_id: str, broker_hash: bytes, token_hash: bytes, amount_base: int) -> str:
"""
Encode vault deposit(VaultDepositFE) calldata.
VaultDepositFE = (bytes32 accountId, bytes32 brokerHash, bytes32 tokenHash, uint128 tokenAmount)
Solidity: deposit((bytes32,bytes32,bytes32,uint128))
"""
# Function selector for deposit((bytes32,bytes32,bytes32,uint128))
selector = keccak(b"deposit((bytes32,bytes32,bytes32,uint128))")[:4]
# Struct with all static fields — encoded inline (no offset pointer)
# account_id: hex string → bytes32
acct_bytes = bytes.fromhex(account_id.replace("0x", "")).ljust(32, b"\x00")
if len(acct_bytes) > 32:
acct_bytes = acct_bytes[:32]
# broker_hash and token_hash are already bytes32
broker_bytes = broker_hash.rjust(32, b"\x00")[:32]
token_bytes = token_hash.rjust(32, b"\x00")[:32]
# uint128 → padded to 32 bytes
amount_bytes = amount_base.to_bytes(32, "big")
return "0x" + (selector + acct_bytes + broker_bytes + token_bytes + amount_bytes).hex()
def _encode_get_deposit_fee(receiver: str, account_id: str, broker_hash: bytes, token_hash: bytes, amount_base: int) -> str:
"""
Encode getDepositFee(address,(bytes32,bytes32,bytes32,uint128)) calldata.
The vault's getDepositFee requires the receiver address and the full
VaultDepositFE struct to calculate the LayerZero cross-chain fee.
"""
selector = keccak(b"getDepositFee(address,(bytes32,bytes32,bytes32,uint128))")[:4]
# address → left-padded to 32 bytes
recv_bytes = bytes.fromhex(receiver.replace("0x", "")).rjust(32, b"\x00")
# struct fields (all static, encoded inline — no offset pointer)
acct_bytes = bytes.fromhex(account_id.replace("0x", "")).ljust(32, b"\x00")[:32]
broker_bytes = broker_hash.rjust(32, b"\x00")[:32]
token_bytes = token_hash.rjust(32, b"\x00")[:32]
amount_bytes = amount_base.to_bytes(32, "big")
return "0x" + (selector + recv_bytes + acct_bytes + broker_bytes + token_bytes + amount_bytes).hex()
# ── RPC Helper ───────────────────────────────────────────────────────────────
def _get_rpc_url(chain_id: int = 42161) -> str:
"""Get RPC URL for the given chain."""
if chain_id == 42161:
return os.environ.get("ARBITRUM_RPC_URL", DEFAULT_ARBITRUM_RPC)
raise ValueError(f"No RPC configured for chain {chain_id}")
async def _eth_call(to: str, data: str, chain_id: int = 42161) -> str:
"""Execute a read-only eth_call via JSON-RPC."""
rpc_url = _get_rpc_url(chain_id)
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [{"to": to, "data": data}, "latest"],
}
async with aiohttp.ClientSession() as session:
async with session.post(
rpc_url,
json=payload,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
if resp.status >= 400:
body = await resp.text()
raise Exception(f"RPC error {resp.status}: {body}")
result = await resp.json()
if "error" in result:
raise Exception(f"RPC error: {result['error']}")
return result["result"]
# ── Deposit Flow ─────────────────────────────────────────────────────────────
async def deposit_usdc(amount: float) -> dict:
"""
Deposit USDC into Orderly trading account.
Flow:
1. ensure_registered() → account_id
2. getDepositFee() via eth_call
3. TX1: approve(vault, amount) on USDC contract
4. TX2: deposit(accountId, brokerHash, tokenHash, amount) on vault
Args:
amount: USDC amount (e.g. 34.08)
Returns:
dict with approve_tx_hash, deposit_tx_hash, amount_deposited, fee_paid
"""
if not _is_fly_machine():
raise RuntimeError("Not running on a Fly Machine — wallet unavailable")
if amount <= 0:
raise ValueError("Amount must be positive")
chain_id = signing._get_chain_id()
usdc_address = USDC_ADDRESSES.get(chain_id)
if not usdc_address:
raise ValueError(f"No USDC address configured for chain {chain_id}")
# 1. Ensure registered
account_id = await signing.ensure_registered()
wallet_address = await signing._get_wallet_address()
logger.info(f"Orderly deposit: account={account_id}, wallet={wallet_address}, amount={amount} USDC")
# 2. Calculate base amount (USDC has 6 decimals)
amount_base = int(amount * (10 ** USDC_DECIMALS))
# 3. Get deposit fee (requires receiver address and full struct)
fee_data = _encode_get_deposit_fee(wallet_address, account_id, BROKER_HASH, TOKEN_HASH, amount_base)
fee_hex = await _eth_call(VAULT_ADDRESS, fee_data, chain_id)
fee_wei = int(fee_hex, 16)
logger.info(f"Orderly deposit fee: {fee_wei} wei ({fee_wei / 1e18:.8f} ETH)")
# 4. TX1: approve
approve_data = _encode_approve(VAULT_ADDRESS, amount_base)
logger.info(f"Orderly deposit: sending approve TX (amount_base={amount_base})")
approve_result = await _wallet_request("POST", "/agent/transfer", {
"to": usdc_address,
"amount": "0",
"data": approve_data,
"chain_id": chain_id,
})
approve_tx = approve_result.get("tx_hash", approve_result.get("hash", "unknown"))
logger.info(f"Orderly deposit: approve TX = {approve_tx}")
# 5. TX2: deposit
deposit_data = _encode_deposit(account_id, BROKER_HASH, TOKEN_HASH, amount_base)
logger.info(f"Orderly deposit: sending deposit TX (fee={fee_wei} wei)")
deposit_result = await _wallet_request("POST", "/agent/transfer", {
"to": VAULT_ADDRESS,
"amount": str(fee_wei),
"data": deposit_data,
"chain_id": chain_id,
})
deposit_tx = deposit_result.get("tx_hash", deposit_result.get("hash", "unknown"))
logger.info(f"Orderly deposit: deposit TX = {deposit_tx}")
return {
"approve_tx_hash": approve_tx,
"deposit_tx_hash": deposit_tx,
"amount_deposited": amount,
"amount_base_units": amount_base,
"fee_paid_wei": fee_wei,
"chain_id": chain_id,
}
# ── Withdraw Flow ────────────────────────────────────────────────────────────
async def withdraw_usdc(amount: float) -> dict:
"""
Withdraw USDC from Orderly trading account.
Flow:
1. ensure_registered() → account_id, wallet_address
2. GET /v1/withdraw_nonce (Ed25519 signed)
3. Sign EIP-712 Withdraw message via Privy wallet
4. POST /v1/withdraw_request (Ed25519 signed)
Args:
amount: USDC amount (e.g. 10.0)
Returns:
dict with withdraw response and amount
"""
if not _is_fly_machine():
raise RuntimeError("Not running on a Fly Machine — wallet unavailable")
if amount <= 0:
raise ValueError("Amount must be positive")
chain_id = signing._get_chain_id()
# 1. Ensure registered
account_id = await signing.ensure_registered()
wallet_address = await signing._get_wallet_address()
logger.info(f"Orderly withdraw: account={account_id}, amount={amount} USDC")
# 2. Get withdraw nonce
nonce = await signing.get_withdraw_nonce()
logger.info(f"Orderly withdraw: nonce={nonce}")
# 3. Build and sign EIP-712 Withdraw message
timestamp = int(time.time() * 1000)
amount_base = int(amount * (10 ** USDC_DECIMALS))
message = {
"brokerId": signing._get_broker_id(),
"chainId": chain_id,
"receiver": wallet_address,
"token": "USDC",
"amount": amount_base,
"withdrawNonce": nonce,
"timestamp": timestamp,
}
result = await _wallet_request("POST", "/agent/sign-typed-data", {
"domain": signing.WITHDRAW_DOMAIN,
"types": signing.WITHDRAW_TYPES,
"primaryType": "Withdraw",
"message": message,
})
signature = signing._reconstruct_signature(result)
logger.info(f"Orderly withdraw: EIP-712 signature obtained")
# 4. POST /v1/withdraw_request
from .client import _get_client
client = _get_client()
withdraw_body = {
"signature": signature,
"userAddress": wallet_address,
"verifyingContract": signing.WITHDRAW_DOMAIN["verifyingContract"],
"message": message,
}
withdraw_result = await client._private_request(
"POST", "/v1/withdraw_request", body=withdraw_body
)
logger.info(f"Orderly withdraw: result={withdraw_result}")
return {
"withdraw_result": withdraw_result,
"amount": amount,
"amount_base_units": amount_base,
"chain_id": chain_id,
"receiver": wallet_address,
}
"""
Orderly Network Signing — Ed25519 request signing + Privy EIP-712 registration.
Two responsibilities:
1. Auto-provision: Generate Ed25519 key pair in memory, register account with
Orderly via Privy EIP-712 signing, and add the key — all on first use.
2. Ongoing signing: Sign every private API request with the in-memory Ed25519 key.
No API key env vars needed — keys are auto-provisioned via Privy on container boot.
"""
import asyncio
import base64
import logging
import os
import time
from typing import Optional, Tuple
import base58
from nacl.signing import SigningKey
from tools.wallet import _wallet_request, _is_fly_machine
logger = logging.getLogger(__name__)
# ── Configuration ─────────────────────────────────────────────────────────────
DEFAULT_API_URL = "https://api.orderly.org"
DEFAULT_BROKER_ID = "woofi_pro"
DEFAULT_CHAIN_ID = 42161 # Arbitrum
ORDERLY_DOMAIN = {
"name": "Orderly",
"version": "1",
"chainId": DEFAULT_CHAIN_ID,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
}
REGISTRATION_TYPES = {
"Registration": [
{"name": "brokerId", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "timestamp", "type": "uint64"},
{"name": "registrationNonce", "type": "uint256"},
]
}
ADD_KEY_TYPES = {
"AddOrderlyKey": [
{"name": "brokerId", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "orderlyKey", "type": "string"},
{"name": "scope", "type": "string"},
{"name": "timestamp", "type": "uint64"},
{"name": "expiration", "type": "uint64"},
]
}
WITHDRAW_DOMAIN = {
"name": "Orderly",
"version": "1",
"chainId": DEFAULT_CHAIN_ID,
"verifyingContract": "0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203",
}
WITHDRAW_TYPES = {
"Withdraw": [
{"name": "brokerId", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "receiver", "type": "address"},
{"name": "token", "type": "string"},
{"name": "amount", "type": "uint256"},
{"name": "withdrawNonce", "type": "uint64"},
{"name": "timestamp", "type": "uint64"},
]
}
# ── Module-level state (in-memory, lives as long as the process) ─────────────
_signing_key: Optional[SigningKey] = None
_public_key_b64: Optional[str] = None # "ed25519:<base58>"
_account_id: Optional[str] = None
_wallet_address: Optional[str] = None
_registered: bool = False
_registration_lock = asyncio.Lock()
def _get_api_url() -> str:
return os.environ.get("ORDERLY_API_URL", DEFAULT_API_URL)
def _get_broker_id() -> str:
return os.environ.get("ORDERLY_BROKER_ID", DEFAULT_BROKER_ID)
def _get_chain_id() -> int:
return int(os.environ.get("ORDERLY_CHAIN_ID", DEFAULT_CHAIN_ID))
def _get_domain() -> dict:
"""Get EIP-712 domain with configured chain ID."""
chain_id = _get_chain_id()
return {**ORDERLY_DOMAIN, "chainId": chain_id}
# ── Ed25519 Key Management ───────────────────────────────────────────────────
def _ensure_keypair() -> Tuple[SigningKey, str]:
"""Generate Ed25519 key pair if not already created."""
global _signing_key, _public_key_b64
if _signing_key is not None:
return _signing_key, _public_key_b64
_signing_key = SigningKey.generate()
pub_bytes = _signing_key.verify_key.encode()
_public_key_b64 = "ed25519:" + base58.b58encode(pub_bytes).decode()
logger.info(f"Generated Orderly Ed25519 key: {_public_key_b64[:30]}...")
return _signing_key, _public_key_b64
def _reconstruct_signature(result: dict) -> str:
"""Reconstruct flat hex signature from wallet service response."""
sig = result.get("signature", "")
if isinstance(sig, str):
hex_clean = sig.replace("0x", "")
if len(hex_clean) == 130:
return sig if sig.startswith("0x") else f"0x{hex_clean}"
if isinstance(sig, dict):
r = sig.get("r", "").replace("0x", "").zfill(64)
s = sig.get("s", "").replace("0x", "").zfill(64)
v = sig.get("v", 0)
if isinstance(v, str):
v = int(v, 16) if v.startswith("0x") else int(v)
if v < 27:
v += 27 # Normalize 0/1 → 27/28
return f"0x{r}{s}{v:02x}"
raise ValueError(f"Cannot parse signature: {result}")
# ── Request Signing (Ed25519) ────────────────────────────────────────────────
def sign_request(timestamp: int, method: str, path: str, body: str = "") -> str:
"""
Sign an API request with the in-memory Ed25519 key.
Orderly signature format: base64(ed25519_sign(timestamp + method + path + body))
"""
key, _ = _ensure_keypair()
message = f"{timestamp}{method.upper()}{path}{body}"
signed = key.sign(message.encode())
# signed.signature is the 64-byte signature, Orderly expects standard base64
return base64.b64encode(signed.signature).decode()
def build_auth_headers(
method: str, path: str, body: str = ""
) -> dict:
"""
Build full authentication headers for a private Orderly API request.
Returns dict with: orderly-timestamp, orderly-account-id, orderly-key, orderly-signature
"""
if not _registered or not _account_id:
raise RuntimeError("Orderly account not registered — call ensure_registered() first")
_, pub_key = _ensure_keypair()
timestamp = int(time.time() * 1000)
signature = sign_request(timestamp, method, path, body)
headers = {
"orderly-timestamp": str(timestamp),
"orderly-account-id": _account_id,
"orderly-key": pub_key,
"orderly-signature": signature,
}
logger.debug(f"Orderly auth: {method} {path}, account={_account_id}, key={pub_key[:30]}...")
return headers
# ── Privy-Based Registration Flow ────────────────────────────────────────────
async def _get_wallet_address() -> str:
"""Get the agent's EVM address from Privy wallet service (cached)."""
global _wallet_address
if _wallet_address:
return _wallet_address
if not _is_fly_machine():
raise RuntimeError("Not running on Fly — wallet unavailable")
data = await _wallet_request("GET", "/agent/wallet")
wallets = data if isinstance(data, list) else data.get("wallets", [])
for w in wallets:
if w.get("chain_type") == "ethereum":
_wallet_address = w["wallet_address"]
return _wallet_address
raise RuntimeError("No ethereum wallet found")
async def _register_account(address: str) -> str:
"""
Register account with Orderly Network via Privy EIP-712 signing.
Steps:
1. GET /v1/registration_nonce
2. Sign EIP-712 Registration message via Privy
3. POST /v1/register_account
Returns: account_id
"""
import aiohttp
api_url = _get_api_url()
broker_id = _get_broker_id()
chain_id = _get_chain_id()
# 1. Get registration nonce
async with aiohttp.ClientSession() as session:
url = f"{api_url}/v1/registration_nonce"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status >= 400:
body = await resp.text()
raise Exception(f"Failed to get registration nonce: HTTP {resp.status}: {body}")
data = await resp.json()
reg_nonce = data["data"]["registration_nonce"]
# 2. Sign EIP-712 Registration via Privy
timestamp = int(time.time() * 1000)
message = {
"brokerId": broker_id,
"chainId": chain_id,
"timestamp": timestamp,
"registrationNonce": int(reg_nonce),
}
result = await _wallet_request("POST", "/agent/sign-typed-data", {
"domain": _get_domain(),
"types": REGISTRATION_TYPES,
"primaryType": "Registration",
"message": message,
})
signature = _reconstruct_signature(result)
# 3. POST register_account
async with aiohttp.ClientSession() as session:
url = f"{api_url}/v1/register_account"
payload = {
"message": message,
"signature": signature,
"userAddress": address,
}
async with session.post(
url, json=payload, timeout=aiohttp.ClientTimeout(total=15)
) as resp:
if resp.status >= 400:
body = await resp.text()
raise Exception(f"Failed to register account: HTTP {resp.status}: {body}")
data = await resp.json()
account_id = data.get("data", {}).get("account_id")
if not account_id:
raise Exception(f"No account_id in registration response: {data}")
logger.info(f"Orderly account registered: {account_id}")
return account_id
async def _add_orderly_key(address: str, public_key: str) -> None:
"""
Add Ed25519 key to Orderly account via Privy EIP-712 signing.
Steps:
1. Sign EIP-712 AddOrderlyKey message via Privy
2. POST /v1/orderly_key
"""
import aiohttp
api_url = _get_api_url()
broker_id = _get_broker_id()
chain_id = _get_chain_id()
timestamp = int(time.time() * 1000)
# Key expires in 30 days
expiration = timestamp + (30 * 24 * 60 * 60 * 1000)
message = {
"brokerId": broker_id,
"chainId": chain_id,
"orderlyKey": public_key,
"scope": "read,trading",
"timestamp": timestamp,
"expiration": expiration,
}
logger.info(f"Orderly _add_orderly_key: signing EIP-712 AddOrderlyKey via wallet service...")
result = await _wallet_request("POST", "/agent/sign-typed-data", {
"domain": _get_domain(),
"types": ADD_KEY_TYPES,
"primaryType": "AddOrderlyKey",
"message": message,
})
logger.info(f"Orderly _add_orderly_key: wallet sign result keys={list(result.keys()) if isinstance(result, dict) else type(result)}")
signature = _reconstruct_signature(result)
logger.info(f"Orderly _add_orderly_key: reconstructed sig len={len(signature)}")
async with aiohttp.ClientSession() as session:
url = f"{api_url}/v1/orderly_key"
payload = {
"message": message,
"signature": signature,
"userAddress": address,
}
async with session.post(
url, json=payload, timeout=aiohttp.ClientTimeout(total=15)
) as resp:
if resp.status >= 400:
body = await resp.text()
raise Exception(f"Failed to add orderly key: HTTP {resp.status}: {body}")
data = await resp.json()
if not data.get("success", True):
raise Exception(f"Failed to add orderly key: {data.get('message', data)}")
logger.info(f"Orderly key added: response={data}")
return data
async def _check_account_exists(address: str) -> Optional[str]:
"""Check if account already exists and return account_id if so."""
import aiohttp
api_url = _get_api_url()
broker_id = _get_broker_id()
async with aiohttp.ClientSession() as session:
url = f"{api_url}/v1/get_account?address={address}&broker_id={broker_id}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status == 200:
data = await resp.json()
if data.get("success") and data.get("data", {}).get("account_id"):
return data["data"]["account_id"]
return None
async def ensure_registered() -> str:
"""
Ensure the agent is registered with Orderly and has an active Ed25519 key.
Orchestrates the full flow:
1. Generate Ed25519 key pair (in memory)
2. Get wallet address from Privy
3. Check if account exists, register if not
4. Add Ed25519 key to account
5. Return account_id
Idempotent — safe to call multiple times. Uses asyncio.Lock to prevent
concurrent registration attempts from parallel tool calls.
"""
global _account_id, _registered
if _registered and _account_id:
return _account_id # Fast path, no lock
async with _registration_lock:
if _registered and _account_id:
return _account_id # Double-check after acquiring lock
# 1. Generate key pair
_, public_key = _ensure_keypair()
# 2. Get wallet address
address = await _get_wallet_address()
logger.info(f"Orderly registration: wallet address = {address}")
# 3. Check if account exists first, register only if needed
existing = await _check_account_exists(address)
if existing:
_account_id = existing
logger.info(f"Orderly account already exists: {_account_id}")
else:
logger.info(f"Orderly account not found, registering...")
_account_id = await _register_account(address)
# 4. Add Ed25519 key (always — key is ephemeral per container boot)
try:
await _add_orderly_key(address, public_key)
except Exception as e:
logger.error(f"Orderly _add_orderly_key FAILED: {e}")
raise
_registered = True
logger.info(f"Orderly ready: account={_account_id}, key={public_key[:30]}...")
return _account_id
def get_account_id() -> Optional[str]:
"""Get the cached account ID (None if not yet registered)."""
return _account_id
async def get_withdraw_nonce() -> int:
"""
Get the next withdraw nonce from Orderly API.
Requires Ed25519 registration to be complete.
"""
import aiohttp
if not _registered or not _account_id:
raise RuntimeError("Orderly account not registered — call ensure_registered() first")
api_url = _get_api_url()
path = "/v1/withdraw_nonce"
headers = build_auth_headers("GET", path)
async with aiohttp.ClientSession() as session:
url = f"{api_url}{path}"
async with session.get(
url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)
) as resp:
if resp.status >= 400:
body = await resp.text()
raise Exception(f"Failed to get withdraw nonce: HTTP {resp.status}: {body}")
data = await resp.json()
nonce = data.get("data", {}).get("withdraw_nonce")
if nonce is None:
raise Exception(f"No withdraw_nonce in response: {data}")
return int(nonce)
"""
Orderly Network Trading Tools — BaseTool subclasses for agent use.
Public tools (8): orderly_system_info, orderly_futures, orderly_funding,
orderly_volume, orderly_orderbook, orderly_kline,
orderly_market, orderly_chain_info
Private tools (6): orderly_account, orderly_holdings, orderly_positions,
orderly_orders, orderly_trades, orderly_liquidations
Trading tools (5): orderly_order, orderly_modify, orderly_cancel,
orderly_cancel_all, orderly_leverage
Fund tools (2): orderly_deposit, orderly_withdraw
"""
import logging
from core.tool import BaseTool, ToolContext, ToolResult
from .client import OrderlyClient, _get_client
logger = logging.getLogger(__name__)
# ── Public / Market Data Tools (8) — No Auth ────────────────────────────────
class OrderlySystemInfoTool(BaseTool):
"""Get Orderly system maintenance status."""
@property
def name(self) -> str:
return "orderly_system_info"
@property
def description(self) -> str:
return """Get Orderly Network system status and maintenance information.
Use this to check if the exchange is operational before placing trades.
Returns: system status, maintenance windows"""
@property
def parameters(self) -> dict:
return {"type": "object", "properties": {}}
async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:
try:
client = _get_client()
data = await client.get_system_info()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyFuturesTool(BaseTool):
"""Get futures instrument info."""
@property
def name(self) -> str:
return "orderly_futures"
@property
def description(self) -> str:
return """Get Orderly futures instrument information.
If symbol is specified, returns details for that instrument. Otherwise returns all futures.
Parameters:
- symbol: (optional) Specific symbol like "PERP_BTC_USDC". Or just "BTC" — auto-expanded.
Returns: instrument details (tick sizes, lot sizes, max leverage, base/quote info)"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Symbol (e.g. 'BTC' or 'PERP_BTC_USDC'). Omit for all.",
},
},
}
async def execute(self, ctx: ToolContext, symbol: str = "", **kwargs) -> ToolResult:
try:
client = _get_client()
if symbol:
resolved = client._resolve_symbol(symbol)
data = await client.get_futures(resolved)
else:
data = await client.get_futures()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyFundingTool(BaseTool):
"""Get funding rate info."""
@property
def name(self) -> str:
return "orderly_funding"
@property
def description(self) -> str:
return """Get funding rate information for Orderly perps.
Shows current funding rate and recent history. Positive = longs pay shorts.
Parameters:
- symbol: Asset name or full symbol (required, e.g. "BTC" or "PERP_BTC_USDC")
- history: If true, also fetch funding rate history (default: false)
Returns: current funding rate, and optionally historical rates"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (e.g. 'BTC') or full symbol",
},
"history": {
"type": "boolean",
"description": "Include funding history (default: false)",
},
},
"required": ["symbol"],
}
async def execute(
self, ctx: ToolContext, symbol: str = "", history: bool = False, **kwargs
) -> ToolResult:
if not symbol:
return ToolResult(success=False, error="'symbol' is required")
try:
client = _get_client()
resolved = client._resolve_symbol(symbol)
current = await client.get_funding_rate(resolved)
result = {"current": current}
if history:
hist = await client.get_funding_rate_history(resolved)
result["history"] = hist
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyVolumeTool(BaseTool):
"""Get volume statistics."""
@property
def name(self) -> str:
return "orderly_volume"
@property
def description(self) -> str:
return """Get Orderly Network volume statistics.
Returns: 24h volume, open interest, and other aggregate stats"""
@property
def parameters(self) -> dict:
return {"type": "object", "properties": {}}
async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:
try:
client = _get_client()
data = await client.get_volume_stats()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyOrderbookTool(BaseTool):
"""Get orderbook snapshot."""
@property
def name(self) -> str:
return "orderly_orderbook"
@property
def description(self) -> str:
return """Get the orderbook for an Orderly asset.
Shows current bid/ask levels with sizes. Useful for checking liquidity and spread.
Parameters:
- symbol: Asset name or full symbol (required, e.g. "BTC" or "PERP_BTC_USDC")
- max_level: Number of price levels (default: 20)
Returns: asks and bids arrays with [price, quantity] entries"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (e.g. 'BTC') or full symbol",
},
"max_level": {
"type": "integer",
"description": "Number of price levels (default: 20)",
},
},
"required": ["symbol"],
}
async def execute(
self, ctx: ToolContext, symbol: str = "", max_level: int = 20, **kwargs
) -> ToolResult:
if not symbol:
return ToolResult(success=False, error="'symbol' is required")
try:
client = _get_client()
resolved = client._resolve_symbol(symbol)
data = await client.get_orderbook(resolved, max_level)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyKlineTool(BaseTool):
"""Get OHLCV candlestick data."""
@property
def name(self) -> str:
return "orderly_kline"
@property
def description(self) -> str:
return """Get OHLCV candlestick data for an Orderly asset.
Use this for price analysis, charting, and identifying trends.
Parameters:
- symbol: Asset name or full symbol (required, e.g. "BTC" or "PERP_BTC_USDC")
- interval: Candle interval (default: "1h"). Options: 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d, 1w
- limit: Number of candles (default: 100, max: 1000)
Returns: array of candles with open, high, low, close, volume, timestamp"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (e.g. 'BTC') or full symbol",
},
"interval": {
"type": "string",
"description": "Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d, 1w (default: 1h)",
},
"limit": {
"type": "integer",
"description": "Number of candles (default: 100)",
},
},
"required": ["symbol"],
}
async def execute(
self,
ctx: ToolContext,
symbol: str = "",
interval: str = "1h",
limit: int = 100,
**kwargs,
) -> ToolResult:
if not symbol:
return ToolResult(success=False, error="'symbol' is required")
try:
client = _get_client()
resolved = client._resolve_symbol(symbol)
data = await client.get_kline(resolved, interval, limit)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyMarketTool(BaseTool):
"""Get market overview — futures + recent trades."""
@property
def name(self) -> str:
return "orderly_market"
@property
def description(self) -> str:
return """Get market overview for Orderly futures.
If symbol is specified, returns instrument details and recent market trades.
Otherwise returns all futures instruments.
Parameters:
- symbol: (optional) Asset name (e.g. "BTC") or full symbol. Omit for all instruments.
Returns: futures info and optionally recent market trades"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (e.g. 'BTC'). Omit for all.",
},
},
}
async def execute(self, ctx: ToolContext, symbol: str = "", **kwargs) -> ToolResult:
try:
client = _get_client()
if symbol:
resolved = client._resolve_symbol(symbol)
futures = await client.get_futures(resolved)
trades = await client.get_market_trades(resolved, limit=20)
return ToolResult(
success=True,
output={"instrument": futures, "recent_trades": trades},
)
else:
data = await client.get_futures()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyChainInfoTool(BaseTool):
"""Get chain and broker configuration."""
@property
def name(self) -> str:
return "orderly_chain_info"
@property
def description(self) -> str:
return """Get Orderly chain and broker configuration.
Returns: supported chains, broker info, deposit/withdrawal config"""
@property
def parameters(self) -> dict:
return {"type": "object", "properties": {}}
async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:
try:
client = _get_client()
data = await client.get_chain_info()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Private / Account Tools (6) — Ed25519 Auth ──────────────────────────────
class OrderlyAccountTool(BaseTool):
"""Get account info (fees, tier, etc.)."""
@property
def name(self) -> str:
return "orderly_account"
@property
def description(self) -> str:
return """Get Orderly account information: fee tier, maker/taker rates, account status.
Use this to check your fee rates and account tier.
Returns: account_id, fee tier, maker/taker fee rates, account status"""
@property
def parameters(self) -> dict:
return {"type": "object", "properties": {}}
async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:
try:
client = _get_client()
data = await client.get_account_info()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyHoldingsTool(BaseTool):
"""Get asset balances (holdings)."""
@property
def name(self) -> str:
return "orderly_holdings"
@property
def description(self) -> str:
return """Get Orderly asset balances (holdings).
Shows available and frozen balances for all assets in the account.
Returns: array of holdings with token, holding (available), frozen, pending_short"""
@property
def parameters(self) -> dict:
return {"type": "object", "properties": {}}
async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:
try:
client = _get_client()
data = await client.get_holdings()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyPositionsTool(BaseTool):
"""Get open positions."""
@property
def name(self) -> str:
return "orderly_positions"
@property
def description(self) -> str:
return """Get all open positions on Orderly.
Use this to check current perp portfolio, position sizes, entry prices, and unrealized PnL.
Returns: array of positions with symbol, position_qty, cost_position, unsettled_pnl, mark_price"""
@property
def parameters(self) -> dict:
return {"type": "object", "properties": {}}
async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:
try:
client = _get_client()
data = await client.get_positions()
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyOrdersTool(BaseTool):
"""List orders."""
@property
def name(self) -> str:
return "orderly_orders"
@property
def description(self) -> str:
return """Get orders on Orderly (open and/or historical).
Parameters:
- symbol: (optional) Filter by symbol (e.g. "BTC" or "PERP_BTC_USDC")
- status: (optional) Filter by status: "INCOMPLETE" (open), "COMPLETED", "CANCELLED"
- limit: Number of orders to return (default: 50)
Returns: array of orders with order_id, symbol, side, type, price, quantity, status"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Symbol filter (e.g. 'BTC'). Omit for all.",
},
"status": {
"type": "string",
"enum": ["INCOMPLETE", "COMPLETED", "CANCELLED"],
"description": "Order status filter",
},
"limit": {
"type": "integer",
"description": "Number of orders (default: 50)",
},
},
}
async def execute(
self,
ctx: ToolContext,
symbol: str = "",
status: str = "",
limit: int = 50,
**kwargs,
) -> ToolResult:
try:
client = _get_client()
resolved = client._resolve_symbol(symbol) if symbol else None
data = await client.get_orders(
symbol=resolved, status=status or None, limit=limit
)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyTradesTool(BaseTool):
"""Get trade/fill history."""
@property
def name(self) -> str:
return "orderly_trades"
@property
def description(self) -> str:
return """Get trade (fill) history on Orderly.
Use this to verify if orders were filled, check execution prices, or review trade history.
Parameters:
- symbol: (optional) Filter by symbol (e.g. "BTC" or "PERP_BTC_USDC")
- limit: Number of trades to return (default: 50)
Returns: array of trades with symbol, side, executed_price, executed_quantity, fee, timestamp"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Symbol filter (e.g. 'BTC'). Omit for all.",
},
"limit": {
"type": "integer",
"description": "Number of trades (default: 50)",
},
},
}
async def execute(
self, ctx: ToolContext, symbol: str = "", limit: int = 50, **kwargs
) -> ToolResult:
try:
client = _get_client()
resolved = client._resolve_symbol(symbol) if symbol else None
data = await client.get_trades(symbol=resolved, limit=limit)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyLiquidationsTool(BaseTool):
"""Get liquidation history."""
@property
def name(self) -> str:
return "orderly_liquidations"
@property
def description(self) -> str:
return """Get liquidation history for the account.
Use this to review past liquidation events.
Parameters:
- limit: Number of liquidations to return (default: 50)
Returns: array of liquidation records"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Number of liquidations (default: 50)",
},
},
}
async def execute(self, ctx: ToolContext, limit: int = 50, **kwargs) -> ToolResult:
try:
client = _get_client()
data = await client.get_liquidations(limit=limit)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Trading Tools (5) — Ed25519 Auth ────────────────────────────────────────
class OrderlyOrderTool(BaseTool):
"""Create an order (limit/market/IOC/FOK/post-only)."""
@property
def name(self) -> str:
return "orderly_order"
@property
def description(self) -> str:
return """Place an order on Orderly Network (perpetual futures or spot).
Parameters:
- symbol: Asset name or full symbol (required, e.g. "BTC" or "PERP_BTC_USDC")
- side: "buy" or "sell" (required)
- order_type: "LIMIT", "MARKET", "IOC", "FOK", "POST_ONLY" (default: "LIMIT")
- quantity: Order size in base asset (required, e.g. 0.01 for 0.01 BTC)
- price: Limit price (required for LIMIT, IOC, FOK, POST_ONLY. Omit for MARKET)
- reduce_only: If true, only reduces existing position (default: false)
Order types:
- LIMIT: Rests on book until filled or cancelled (GTC)
- MARKET: Fills at best available price immediately
- IOC: Immediate-or-Cancel (fill what's available, cancel rest)
- FOK: Fill-or-Kill (fill entirely or cancel entirely)
- POST_ONLY: Rejected if it would immediately fill (maker only)
Returns: order_id, status, filled quantity"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (e.g. 'BTC') or full symbol (e.g. 'PERP_BTC_USDC')",
},
"side": {
"type": "string",
"enum": ["buy", "sell"],
"description": "Order side",
},
"order_type": {
"type": "string",
"enum": ["LIMIT", "MARKET", "IOC", "FOK", "POST_ONLY"],
"description": "Order type (default: LIMIT)",
},
"quantity": {
"type": "number",
"description": "Order size in base asset",
},
"price": {
"type": "number",
"description": "Limit price (omit for MARKET orders)",
},
"reduce_only": {
"type": "boolean",
"description": "Reduce-only (default: false)",
},
},
"required": ["symbol", "side", "quantity"],
}
async def execute(
self,
ctx: ToolContext,
symbol: str = "",
side: str = "",
order_type: str = "LIMIT",
quantity: float = 0,
price: float = None,
reduce_only: bool = False,
**kwargs,
) -> ToolResult:
if not symbol or not side or not quantity:
return ToolResult(
success=False, error="'symbol', 'side', and 'quantity' are required"
)
try:
client = _get_client()
resolved = client._resolve_symbol(symbol)
data = await client.create_order(
symbol=resolved,
side=side.upper(),
order_type=order_type.upper(),
order_quantity=quantity,
order_price=price,
reduce_only=reduce_only,
)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyModifyTool(BaseTool):
"""Modify an existing order."""
@property
def name(self) -> str:
return "orderly_modify"
@property
def description(self) -> str:
return """Modify an existing order on Orderly (change price or quantity).
Parameters:
- order_id: Order ID to modify (required — get from orderly_orders)
- symbol: Asset name or full symbol (required)
- side: "buy" or "sell" (required)
- order_type: "LIMIT", "IOC", "FOK", "POST_ONLY" (default: "LIMIT")
- quantity: New order quantity (optional)
- price: New limit price (optional)
Returns: modified order confirmation"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"order_id": {
"type": "integer",
"description": "Order ID to modify",
},
"symbol": {
"type": "string",
"description": "Asset name or full symbol",
},
"side": {
"type": "string",
"enum": ["buy", "sell"],
"description": "Order side",
},
"order_type": {
"type": "string",
"enum": ["LIMIT", "IOC", "FOK", "POST_ONLY"],
"description": "Order type (default: LIMIT)",
},
"quantity": {
"type": "number",
"description": "New order quantity",
},
"price": {
"type": "number",
"description": "New limit price",
},
},
"required": ["order_id", "symbol", "side"],
}
async def execute(
self,
ctx: ToolContext,
order_id: int = 0,
symbol: str = "",
side: str = "",
order_type: str = "LIMIT",
quantity: float = None,
price: float = None,
**kwargs,
) -> ToolResult:
if not order_id or not symbol or not side:
return ToolResult(
success=False,
error="'order_id', 'symbol', and 'side' are required",
)
try:
client = _get_client()
resolved = client._resolve_symbol(symbol)
data = await client.modify_order(
order_id=order_id,
symbol=resolved,
side=side.upper(),
order_type=order_type.upper(),
order_quantity=quantity,
order_price=price,
)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyCancelTool(BaseTool):
"""Cancel an order."""
@property
def name(self) -> str:
return "orderly_cancel"
@property
def description(self) -> str:
return """Cancel an open order on Orderly by order ID.
Parameters:
- symbol: Asset name or full symbol (required, e.g. "BTC" or "PERP_BTC_USDC")
- order_id: Order ID to cancel (required — get from orderly_orders)
Returns: cancel confirmation"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (e.g. 'BTC') or full symbol",
},
"order_id": {
"type": "integer",
"description": "Order ID to cancel",
},
},
"required": ["symbol", "order_id"],
}
async def execute(
self, ctx: ToolContext, symbol: str = "", order_id: int = 0, **kwargs
) -> ToolResult:
if not symbol or not order_id:
return ToolResult(
success=False, error="'symbol' and 'order_id' are required"
)
try:
client = _get_client()
resolved = client._resolve_symbol(symbol)
data = await client.cancel_order(resolved, order_id)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyCancelAllTool(BaseTool):
"""Cancel all open orders."""
@property
def name(self) -> str:
return "orderly_cancel_all"
@property
def description(self) -> str:
return """Cancel all open orders on Orderly.
Parameters:
- symbol: (optional) Asset name to cancel orders for. Omit to cancel ALL orders.
Returns: cancel confirmation"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (optional — omit for all)",
},
},
}
async def execute(self, ctx: ToolContext, symbol: str = "", **kwargs) -> ToolResult:
try:
client = _get_client()
resolved = client._resolve_symbol(symbol) if symbol else None
data = await client.cancel_all_orders(resolved)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyLeverageTool(BaseTool):
"""Update leverage for a symbol."""
@property
def name(self) -> str:
return "orderly_leverage"
@property
def description(self) -> str:
return """Set leverage for an Orderly perpetual asset.
Parameters:
- symbol: Asset name or full symbol (required, e.g. "BTC" or "PERP_BTC_USDC")
- leverage: Leverage multiplier (required, e.g. 5 for 5x)
Returns: leverage update confirmation"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Asset name (e.g. 'BTC') or full symbol",
},
"leverage": {
"type": "integer",
"description": "Leverage multiplier (e.g. 5)",
},
},
"required": ["symbol", "leverage"],
}
async def execute(
self,
ctx: ToolContext,
symbol: str = "",
leverage: int = 0,
**kwargs,
) -> ToolResult:
if not symbol or not leverage:
return ToolResult(
success=False, error="'symbol' and 'leverage' are required"
)
try:
client = _get_client()
resolved = client._resolve_symbol(symbol)
data = await client.update_leverage(resolved, leverage)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
# ── Fund Management Tools (2) — Deposit & Withdraw ──────────────────────────
class OrderlyDepositTool(BaseTool):
"""Deposit USDC into Orderly trading account."""
@property
def name(self) -> str:
return "orderly_deposit"
@property
def description(self) -> str:
return """Deposit USDC into Orderly Network trading account from the agent's on-chain wallet.
This performs two on-chain transactions:
1. ERC-20 approve — allows the Orderly vault to spend USDC
2. Vault deposit — transfers USDC into the Orderly trading account
The deposit fee (cross-chain relay cost) is paid in ETH automatically.
Funds typically settle in 1-5 minutes after the deposit transaction confirms.
Parameters:
- amount: USDC amount to deposit (e.g. 34.08)
Returns: approve_tx_hash, deposit_tx_hash, amount_deposited, fee_paid"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"amount": {
"type": "number",
"description": "USDC amount to deposit (e.g. 34.08)",
},
},
"required": ["amount"],
}
async def execute(self, ctx: ToolContext, amount: float = 0, **kwargs) -> ToolResult:
if not amount or amount <= 0:
return ToolResult(success=False, error="'amount' must be a positive number")
try:
from .deposit import deposit_usdc
data = await deposit_usdc(amount)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))
class OrderlyWithdrawTool(BaseTool):
"""Withdraw USDC from Orderly trading account."""
@property
def name(self) -> str:
return "orderly_withdraw"
@property
def description(self) -> str:
return """Withdraw USDC from Orderly Network trading account to the agent's on-chain wallet.
This creates an EIP-712 signed withdrawal request. The funds are sent back to the
agent's wallet address on the configured chain (default: Arbitrum).
Note: Withdrawals may take a few minutes to process. Check orderly_holdings to confirm.
Parameters:
- amount: USDC amount to withdraw (e.g. 10.0)
Returns: withdrawal confirmation with status"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"amount": {
"type": "number",
"description": "USDC amount to withdraw (e.g. 10.0)",
},
},
"required": ["amount"],
}
async def execute(self, ctx: ToolContext, amount: float = 0, **kwargs) -> ToolResult:
if not amount or amount <= 0:
return ToolResult(success=False, error="'amount' must be a positive number")
try:
from .deposit import withdraw_usdc
data = await withdraw_usdc(amount)
return ToolResult(success=True, output=data)
except Exception as e:
return ToolResult(success=False, error=str(e))