Automations

Write custom event-driven trading logic as TypeScript scripts. Automations give you full control over trade logic while OpenBroker handles real-time event delivery and SDK access. The runtime uses WebSocket for instant price updates, order lifecycle, and liquidation alerts, with REST polling as a heartbeat fallback. Designed for AI agents to write and deploy dynamically.

Bundled Examples

OpenBroker ships 6 example automations that demonstrate common trading patterns. These are reference implementations — read the source to learn the API patterns, then write your own automation tailored to your specific requirements.

ExampleDescriptionKey Config
dcaDollar cost averagingcoin, amount, interval, count
funding-arbFunding rate arbitragecoin, sizeUsd, minFunding, closeAt
gridGrid tradingcoin, lower, upper, grids, size, mode
mm-spreadMarket making (spread)coin, size, spreadBps, maxPosition
mm-makerMaker-only MM (ALO)coin, size, offsetBps, maxPosition
price-alertReal-time price & order monitoring via WebSocketcoin, threshold, above, below
# List examples with full config schemas
openbroker auto examples
 
# Run an example with config overrides
openbroker auto run --example dca --set coin=HYPE --set amount=50 --dry

For AI agents: Do not run bundled examples directly as production strategies. Read the example source code to learn the automation API, then write a custom automation with proper risk management, position sizing, and TP/SL logic tailored to the user's requirements. Save custom automations to ~/.openbroker/automations/.

Guidelines

Rules and guardrails for writing safe, reliable automations — whether by hand or by an AI agent.

Risk & Safety

  • Always monitor for liquidation. Subscribe to the liquidation event (WebSocket-only) for real-time liquidation alerts, and use margin_warning and pnl_threshold as additional safety nets. If no margin/liquidation automation is running, create one before placing trades.
  • Always notify the user. Use api.publish() for important events — position opens/closes, TP/SL triggers, large PnL swings, margin warnings, errors, and anything requiring human attention. Never silently handle critical events.
  • Always clean up on shutdown. Register an api.onStop() handler to cancel open orders and close positions (or alert the user). Never leave orphaned orders or unmanaged positions.
  • Always set TP/SL. Every new position should have take-profit and stop-loss protection, either within the automation or confirmed by the user. Unprotected positions are a liability.
  • Check margin before trading. Never place trades without validating that sufficient margin is available. Query account state before sizing orders.
  • Cap position sizes. Size positions relative to account equity. Don't risk more than a reasonable percentage on a single trade unless the user explicitly specifies the size.

Reliability

  • Use persistent state. Track position state, entry prices, and flags with api.state — not in-memory variables. Automations survive restarts and are automatically restarted.
  • Guard against duplicates. Use api.state.get/set as idempotency checks before placing orders. Events can fire multiple times for the same condition across polls.
  • Handle errors gracefully. The runtime catches errors per handler, but you should still handle expected failures (order rejection, insufficient margin) within handlers.

Communication

  • Use api.publish() for all alerts — never manually construct webhook requests.
  • Publish on every significant action: trade execution, position opened/closed, TP/SL triggered, PnL threshold exceeded, margin warning, and automation errors.
  • Include actionable context: coin, price, size, PnL, and what happened — so the user can make informed decisions without checking the terminal.

Writing an Automation

An automation is a .ts file that exports a default function. The function receives an AutomationAPI object with the full Hyperliquid client, event subscriptions, state persistence, and logging.

~/.openbroker/automations/funding-scalp.ts
export default function(api) {
  const COIN = 'ETH';
 
  api.on('funding_update', async ({ coin, annualized }) => {
    if (coin !== COIN) return;
 
    if (annualized > 0.5 && !api.state.get('isShort')) {
      api.log.info('High positive funding — going short');
      await api.client.marketOrder(COIN, false, 0.1);
      api.state.set('isShort', true);
    }
  });
 
  api.onStop(async () => {
    if (api.state.get('isShort')) {
      api.log.warn('Closing short on shutdown');
      await api.client.marketOrder('ETH', true, 0.1);
      api.state.set('isShort', false);
    }
  });
}

Events

Subscribe to events with api.on(event, handler). Events are delivered in two ways:

  • WebSocket (real-time): Price changes, order updates, and liquidation alerts are delivered instantly via WebSocket. This is the default and primary event source.
  • REST polling (heartbeat): Position changes, funding rates, PnL thresholds, and margin warnings are detected by polling Hyperliquid at a configurable interval (default: 30s with WebSocket, 10s without).

Events only fire when at least one handler is registered — no unnecessary processing.

WebSocket Events

EventPayloadWhen
price_changecoin, oldPrice, newPrice, changePctMid price moved ≥ 0.01% (real-time via WebSocket, falls back to REST polling if WebSocket is off)
order_updatecoin, oid, side, size, price, origSize, status, statusTimestampOrder lifecycle change — status is filled, canceled, rejected, triggered, etc.
liquidationlid, liquidator, liquidatedUser, liquidatedNtlPos, liquidatedAccountValueLiquidation detected — only available via WebSocket (requires --no-ws to be absent)

REST Polling Events

EventPayloadWhen
ticktimestamp, pollCountEvery poll cycle
funding_updatecoin, fundingRate, annualized, premiumEvery poll for all assets with registered handlers
position_openedcoin, side, size, entryPriceNew position detected
position_closedcoin, previousSize, entryPricePosition no longer present
position_changedcoin, oldSize, newSize, entryPricePosition size changed
pnl_thresholdcoin, unrealizedPnl, changePct, positionValuePnL moved > 5% of position value
margin_warningmarginUsedPct, equity, marginUsedMargin usage > 80%

WebSocket fallback: If WebSocket connection fails, the runtime falls back to REST polling automatically. Price changes are then detected via polling instead. The liquidation and order_update events are only available when WebSocket is active.

SDK Access

The api.client property exposes the full Hyperliquid client with 40+ methods for trading, market data, and account management.

Trading
await api.client.marketOrder('ETH', true, 0.1);     // Market buy
await api.client.limitOrder('BTC', false, 0.01, 100000); // Limit sell
await api.client.triggerOrder('ETH', false, 0.1, 3800, true); // Stop loss
await api.client.cancelAll('ETH');                    // Cancel all ETH orders
await api.client.updateLeverage('ETH', 10);           // Set 10x leverage
Market Data
const mids = await api.client.getAllMids();           // All mid prices
const state = await api.client.getUserStateAll();     // Account state
const orders = await api.client.getOpenOrders();      // Open orders
const fills = await api.client.getUserFills();        // Fill history

Utility functions are also available via api.utils: roundPrice, roundSize, sleep, normalizeCoin, formatUsd, formatPercent, annualizeFundingRate.

The api.dryRun boolean indicates whether the automation is running in --dry mode. Use it to adjust behavior — for example, skipping publish notifications during test runs:

if (!api.dryRun) {
  await api.publish(`Opened ${side} ${size} ${COIN} at ${price}`);
}

Audit Trail & Reports

Every automation run writes a local audit trail automatically to:

~/.openbroker/automation-audit.sqlite

The runtime records:

  • Run metadata and config
  • Structured logs and runtime errors
  • Trading write actions like order placement, cancel, leverage changes, TWAP, and spot execution
  • WebSocket order updates, fills, and user events
  • Per-poll account snapshots for equity, margin, and positions
  • Custom notes and metrics recorded from your script

OpenBroker uses a shared local audit daemon to handle all writes to this database. The daemon starts automatically the first time an automation needs it, and later automation runs reuse the same daemon. You do not need to start or manage it manually.

Reading Reports

# Latest run for an automation ID
openbroker auto report hype-mm-v2-live-r4
 
# Specific run
openbroker auto report hype-mm-v2-live-r4 --run 123e4567...
 
# Machine-readable output
openbroker auto report hype-mm-v2-live-r4 --json
 
# Live refresh in the terminal
openbroker auto report hype-mm-v2-live-r4 --watch
openbroker auto report hype-mm-v2-live-r4 --watch --watch-interval 1000

Reports include run status, counts by event type, fill economics, equity snapshots, action breakdowns, and recent logs/fills/order updates.

Custom Audit Notes

Your automation can add custom research breadcrumbs and numeric metrics:

Custom audit notes
api.audit.record('rebalance_decision', {
  reason: 'inventory_drift',
  perpDeltaUsd: 14.2,
  hedgeClipUsd: 10,
});
 
api.audit.metric('spread_bps', 8.5, { coin: 'HYPE', mode: 'maker' });
api.audit.metric('net_delta_usd', 1.8, { coin: 'HYPE' });

This is useful for later benchmarking across strategy versions, especially for market making, funding capture, and hedge quality analysis.

Lifecycle Hooks

api.onStart(async () => {
  api.log.info('Automation started — loading initial state');
});
 
api.onStop(async () => {
  api.log.info('Cleaning up — closing positions');
  // Cleanup logic here
});
 
api.onError(async (error) => {
  api.log.error(`Handler failed: ${error.message}`);
});

Scheduled Tasks

Use api.every(ms, handler) for recurring logic aligned to the poll loop. Useful for DCA-style periodic buys or rebalancing.

// Buy $100 of ETH every hour
api.every(60 * 60 * 1000, async () => {
  const mids = await api.client.getAllMids();
  const price = parseFloat(mids['ETH']);
  const size = api.utils.roundSize(100 / price, 4);
  await api.client.marketOrder('ETH', true, parseFloat(size));
  api.log.info(`DCA buy: ${size} ETH at ${price}`);
});

State Persistence

Simple key-value state that persists across restarts at ~/.openbroker/state/<id>.json.

api.state.set('lastBuyPrice', 3500);
const price = api.state.get<number>('lastBuyPrice');
api.state.delete('lastBuyPrice');
api.state.clear();

Publishing to the Agent

Use api.publish(message, options?) to send messages back to the OpenClaw agent via webhook. This triggers an agent turn — the agent receives the message and can notify the user, take action, or log the event.

// Simple notification
await api.publish(`ETH broke above $4000 — price: ${price}`);
 
// With options
await api.publish(`Margin at ${pct}% — positions at risk`, {
  name: 'margin-alert',           // appears in logs
  wakeMode: 'now',                // 'now' (default) or 'next-heartbeat'
  channel: 'slack',               // target channel (optional)
  deliver: true,                  // send agent response to messaging (default: true)
});

Returns true if delivered, false if webhooks are not configured. Requires OPENCLAW_HOOKS_TOKEN (automatically set when running as an OpenClaw plugin).

Example: Price Breakout

~/.openbroker/automations/breakout.ts
export default function(api) {
  const COIN = 'ETH';
  const BREAKOUT_PCT = 2;  // 2% move triggers entry
  const SIZE = 0.5;
 
  let basePrice: number | null = null;
 
  api.onStart(async () => {
    const mids = await api.client.getAllMids();
    basePrice = parseFloat(mids[COIN]);
    api.log.info(`Watching ${COIN} from ${basePrice} for ${BREAKOUT_PCT}% breakout`);
  });
 
  api.on('price_change', async ({ coin, newPrice, changePct }) => {
    if (coin !== COIN || !basePrice) return;
 
    const totalChange = ((newPrice - basePrice) / basePrice) * 100;
 
    if (Math.abs(totalChange) >= BREAKOUT_PCT && !api.state.get('inPosition')) {
      const side = totalChange > 0;
      api.log.info(`Breakout! ${totalChange.toFixed(2)}% — entering ${side ? 'long' : 'short'}`);
      await api.client.marketOrder(COIN, side, SIZE);
      api.state.set('inPosition', true);
 
      // Notify the agent so it can alert the user
      await api.publish(
        `Breakout detected on ${COIN}: ${totalChange.toFixed(2)}% move. Entered ${side ? 'long' : 'short'} ${SIZE} at ${newPrice.toFixed(2)}`
      );
    }
  });
}

CLI Commands

openbroker auto run my-strategy --dry       # Test without trading
openbroker auto run ./funding-scalp.ts      # Run from path
openbroker auto run my-strategy --poll 5000 # Poll every 5s
openbroker auto run my-strategy --no-ws     # Force REST-only (no WebSocket)
openbroker auto run --example dca --set coin=HYPE --set amount=50 --dry
openbroker auto examples                    # List bundled examples with config
openbroker auto list                        # Show available scripts
openbroker auto status                      # Show running automations
openbroker auto stop <id>                   # Stop a running automation
openbroker auto clean                       # Remove stale entries from registry
FlagDescriptionDefault
--dryIntercept all write methods — no real trades placed
--verboseShow debug output from the runtime
--id <name>Custom automation ID (default: filename without .ts)
--poll <ms>Poll interval in milliseconds30000 (ws on), 10000 (ws off)
--no-wsDisable WebSocket — use REST polling only for all events
--example <name>Run a bundled example (dca, grid, funding-arb, mm-spread, mm-maker, price-alert)
--set key=valuePre-seed config values for the automation (repeatable, won't overwrite persisted state)

Scripts are loaded from ~/.openbroker/automations/ by name, or from any path. The runtime handles graceful shutdown on SIGINT, calling all onStop hooks before exiting.

Managing Automations

Running automations are tracked in a cross-process registry at ~/.openbroker/state/_registry.json. Use auto status to see what's running and auto stop <id> to stop an automation by its ID. If an automation crashes, the registry entry includes the error message — use auto clean to remove stale entries.