Scaffold this example locally and test it with the CLI:
circuit new --name my-hyperliquid-agent --language python --template hyperliquid
cd my-hyperliquid-agent
circuit run # execute a run cycle
circuit unwind # test the unwind logic
circuit.toml
circuit.toml
name = "Example ETH Perp Agent"
tagline = "Buys ETH perps each cycle"
category = "quant"
imageUrl = "https://cdn.circuit.org/agents/default"
walletType = "ethereum"
allowedExecutionModes = ["auto", "manual"]
runtimeIntervalMinutes = 60
[startingAsset]
network = "hypercore:perp" # Hyperliquid Perpetuals
address = "USDC" # USDC on Hyperliquid
minimumAmount = "2000000000" # 20 USDC (8 decimals; Circuit raw units for hypercore:perp USDC)
Example
from circuit_sdk import AgentContext, SessionPortfolio
BUY_SIZE = 0.01 # ETH to buy each cycle
# Fetch the current Hyperliquid midpoint for a coin via the SDK. Values are
# streamed into Circuit once per second, so this avoids hitting Hyperliquid
# directly while still being fresh enough for slippage estimation.
def get_hyperliquid_mid(
agent: AgentContext, coin: str, dex: str | None = None
) -> float:
result = agent.platforms.hyperliquid.midpoint_price(coin, dex)
if not result.success or result.data is None or isinstance(result.data, list):
raise RuntimeError(result.error or f"No mid price for {coin}")
return float(result.data.price_usd)
def run(agent: AgentContext) -> None:
# Check available balance
balances = agent.platforms.hyperliquid.balances()
if not balances.success or not balances.data:
agent.log(balances.error or "Failed to fetch balances", error=True)
return
withdrawable = float(balances.data.perp.withdrawable)
agent.log(f"Withdrawable balance: ${withdrawable:.2f}")
if withdrawable < 20:
agent.log("Not enough balance to place order")
return
# Check existing positions
# Optional: pass a builder DEX name like "xyz", "cash", or "vntl" to scope positions to that venue.
positions = agent.platforms.hyperliquid.positions()
if positions.success and positions.data:
for pos in positions.data:
agent.log(f"Open: {pos.coin} {pos.side} {pos.size} @ {pos.entry_price} (PnL: {pos.unrealized_pnl})")
# Place a market buy for ETH perps
# price acts as slippage limit for market orders — derive it from the live mid
eth_mid = get_hyperliquid_mid(agent, "ETH")
order = agent.platforms.hyperliquid.place_order({
"coin": "ETH",
"side": "buy",
"size": BUY_SIZE,
"price": eth_mid * 1.01, # 1% above mid — tolerated slippage for a market buy
"market": "perp",
"type": "market",
})
if order.success and order.data:
agent.log(f"Order placed: {order.data.order_id} ({order.data.status})")
# Track total buys in memory
prev = agent.memory.get("totalBuys")
count = int(prev.data.value) + 1 if prev.data and prev.data.value is not None else 1
agent.memory.set("totalBuys", str(count))
agent.log(f"Total buy orders placed: {count}")
else:
agent.log(order.error or "Order failed", error=True)
def unwind(agent: AgentContext, portfolio: SessionPortfolio) -> None:
# Close all open perp positions
# Optional: pass a builder DEX name like "xyz", "cash", or "vntl" to close positions for that venue only.
open_positions = agent.platforms.hyperliquid.positions()
if not open_positions.success or not open_positions.data or len(open_positions.data) == 0:
agent.log("No open positions to close")
return
for pos in open_positions.data:
close_side = "sell" if pos.side == "long" else "buy"
# Use entry price with 50% slippage — must stay within 95% of reference price
entry = float(pos.entry_price)
slippage_price = entry * 0.5 if close_side == "sell" else entry * 1.5
close_order = agent.platforms.hyperliquid.place_order({
"coin": pos.coin,
"side": close_side,
"size": float(pos.size),
"price": slippage_price,
"market": "perp",
"type": "market",
"reduceOnly": True,
})
if close_order.success and close_order.data:
agent.log(f"Closed {pos.coin} {pos.side}: {close_order.data.status}")
else:
agent.log(f"Failed to close {pos.coin}: {close_order.error}", error=True)
Sample Output
Agent run started
Withdrawable balance: $96.67
Open: BTC long 0.00031 @ 68237.0 (PnL: -0.26629)
Open: ETH long 0.021 @ 1951.33 (PnL: -0.175046)
Open: SOL long 0.13 @ 84.979 (PnL: -0.24856)
Order placed: 327024162229 (filled)
Total buy orders placed: 1
Agent run completed
Agent unwind started
Closed BTC long: filled
Closed ETH long: filled
Closed SOL long: filled
Agent unwind completed
How It Works
- Check balance: Reads withdrawable USDC from the perp account
- Log positions: Shows any existing open positions with PnL
- Place order: Buys a small ETH perp position with a market order
- Track state: Uses memory to count total buy orders across cycles
- Unwind: Closes all open positions with reduce-only market orders
Notes
- The
pricefield on market orders acts as a slippage limit — set it above current market for buys, below for sells. reduceOnly: trueensures the close order can only reduce an existing position, not open a new one.- Hyperliquid amounts are formatted values (e.g.,
0.01ETH), not raw units like EVM chains. - See Hyperliquid tick and lot sizes for minimum order sizes per symbol.