Skip to main content

circuit.toml

name = "my-eth-perp-agent"
walletType = "ethereum"
runtimeIntervalMinutes = 60
tagline = "Buys ETH perps each cycle"
allowedExecutionModes = ["auto"]
version = "0.0.1"

[startingAsset]
network = "hypercore:perp"
address = "USDC"
minimumAmount = "50000000" # 50 USDC (6 decimals)

Example

import { Agent, type AgentContext, type CurrentPosition } from "@circuitorg/agent-sdk";

const BUY_SIZE = 0.01; // ETH to buy each cycle

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

  // Place a market buy for ETH perps
  // price acts as slippage limit for market orders — set above current market price
  const order = await agent.platforms.hyperliquid.placeOrder({
    symbol: "ETH",
    side: "buy",
    size: BUY_SIZE,
    price: 10000, // Max price willing to pay (slippage limit)
    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.success && prev.data ? 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 });
  }
}

async function unwind(agent: AgentContext, positions: CurrentPosition[]): Promise<void> {
  // Close all open perp positions
  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";
    const closeOrder = await agent.platforms.hyperliquid.placeOrder({
      symbol: pos.symbol,
      side: closeSide,
      size: parseFloat(pos.size),
      price: closeSide === "sell" ? 0.01 : 999999, // Aggressive slippage limit to ensure fill
      market: "perp",
      type: "market",
      reduceOnly: true
    });

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

const agent = new Agent({
  runFunction: run,
  unwindFunction: unwind,
});

export default agent.getExport();

Sample Output

Agent run started
Withdrawable balance: $94.58
Open: BTC long 0.00038 @ 70877.0 (PnL: -1.21448)
Open: ETH long 0.0171 @ 2029.26 (PnL: -0.7228)
Open: SOL long 0.17 @ 87.082 (PnL: -0.75429)
Open: BNB long 0.056 @ 641.41 (PnL: -1.5344)
Order placed: 317896209844 (filled)
Total buy orders placed: 2
Agent run 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 for minimum order sizes per symbol.