> ## Documentation Index
> Fetch the complete documentation index at: https://docs.circuit.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Hyperliquid Agent

> A simple agent that buys ETH perps on Hyperliquid and closes positions on unwind.

<Tip>
  Scaffold this example locally and test it with the CLI:

  ```bash theme={null}
  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
  ```
</Tip>

### circuit.toml

```toml circuit.toml theme={null}
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

<CodeGroup>
  ```python Python theme={null}
  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)

  ```

  ```typescript TypeScript theme={null}
  import type { AgentContext, SessionPortfolio } from "circuit:sdk";

  const 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.
  async function getHyperliquidMid(
    agent: AgentContext,
    coin: string,
    dex?: string,
  ): Promise<number> {
    const result = await agent.platforms.hyperliquid.midpointPrice(coin, dex);
    if (!result.success || !result.data || Array.isArray(result.data)) {
      throw new Error(result.error || `No mid price for ${coin}`);
    }
    return parseFloat(result.data.priceUsd);
  }

  export async function run(agent: AgentContext): Promise<void> {
    // Check available balance
    const balances = await agent.platforms.hyperliquid.balances();
    if (!balances.success || !balances.data) {
      await agent.log(balances.error || "Failed to fetch balances", { error: true });
      return;
    }

    const withdrawable = parseFloat(balances.data.perp.withdrawable);
    await agent.log(`Withdrawable balance: $${withdrawable.toFixed(2)}`);

    if (withdrawable < 20) {
      await 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.
    const positions = await agent.platforms.hyperliquid.positions();
    if (positions.success && positions.data) {
      for (const pos of positions.data) {
        await agent.log(`Open: ${pos.coin} ${pos.side} ${pos.size} @ ${pos.entryPrice} (PnL: ${pos.unrealizedPnl})`);
      }
    }

    // Place a market buy for ETH perps
    // price acts as slippage limit for market orders — derive it from the live mid
    const ethMid = await getHyperliquidMid(agent, "ETH");
    const order = await agent.platforms.hyperliquid.placeOrder({
      coin: "ETH",
      side: "buy",
      size: BUY_SIZE,
      price: ethMid * 1.01, // 1% above mid — tolerated slippage for a market buy
      market: "perp",
      type: "market"
    });

    if (order.success && order.data) {
      await agent.log(`Order placed: ${order.data.orderId} (${order.data.status})`);

      // Track total buys in memory
      const prev = await agent.memory.get("totalBuys");
      const count = prev.data?.value != null ? parseInt(prev.data.value) + 1 : 1;
      await agent.memory.set("totalBuys", count.toString());
      await agent.log(`Total buy orders placed: ${count}`);
    } else {
      await agent.log(order.error || "Order failed", { error: true });
    }
  }

  export async function unwind(agent: AgentContext, portfolio: SessionPortfolio): Promise<void> {
    // Close all open perp positions
    // Optional: pass a builder DEX name like "xyz", "cash", or "vntl" to close positions for that venue only.
    const openPositions = await agent.platforms.hyperliquid.positions();
    if (!openPositions.success || !openPositions.data || openPositions.data.length === 0) {
      await agent.log("No open positions to close");
      return;
    }

    for (const pos of openPositions.data) {
      const closeSide = pos.side === "long" ? "sell" : "buy";
      // Use entry price with 50% slippage — must stay within 95% of reference price
      const entry = parseFloat(pos.entryPrice);
      const slippagePrice = closeSide === "sell" ? entry * 0.5 : entry * 1.5;
      const closeOrder = await agent.platforms.hyperliquid.placeOrder({
        coin: pos.coin,
        side: closeSide,
        size: parseFloat(pos.size),
        price: slippagePrice,
        market: "perp",
        type: "market",
        reduceOnly: true
      });

      if (closeOrder.success && closeOrder.data) {
        await agent.log(`Closed ${pos.coin} ${pos.side}: ${closeOrder.data.status}`);
      } else {
        await agent.log(`Failed to close ${pos.coin}: ${closeOrder.error}`, { error: true });
      }
    }
  }

  ```
</CodeGroup>

### Sample Output

```text theme={null}
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
```

```text theme={null}
Agent unwind started
Closed BTC long: filled
Closed ETH long: filled
Closed SOL long: filled
Agent unwind completed
```

### How It Works

1. **Check balance**: Reads withdrawable USDC from the perp account
2. **Log positions**: Shows any existing open positions with PnL
3. **Place order**: Buys a small ETH perp position with a market order
4. **Track state**: Uses memory to count total buy orders across cycles
5. **Unwind**: Closes all open positions with reduce-only market orders

### Notes

* The `price` field on market orders acts as a slippage limit — set it above current market for buys, below for sells.
* `reduceOnly: true` ensures the close order can only reduce an existing position, not open a new one.
* Hyperliquid amounts are formatted values (e.g., `0.01` ETH), not raw units like EVM chains.
* See [Hyperliquid tick and lot sizes](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size) for minimum order sizes per symbol.
