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.
| Example | Description | Key Config |
|---|---|---|
| dca | Dollar cost averaging | coin, amount, interval, count |
| funding-arb | Funding rate arbitrage | coin, sizeUsd, minFunding, closeAt |
| grid | Grid trading | coin, lower, upper, grids, size, mode |
| mm-spread | Market making (spread) | coin, size, spreadBps, maxPosition |
| mm-maker | Maker-only MM (ALO) | coin, size, offsetBps, maxPosition |
| price-alert | Real-time price & order monitoring via WebSocket | coin, 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 --dryFor 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
liquidationevent (WebSocket-only) for real-time liquidation alerts, and usemargin_warningandpnl_thresholdas 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/setas 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.
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
| Event | Payload | When |
|---|---|---|
| price_change | coin, oldPrice, newPrice, changePct | Mid price moved ≥ 0.01% (real-time via WebSocket, falls back to REST polling if WebSocket is off) |
| order_update | coin, oid, side, size, price, origSize, status, statusTimestamp | Order lifecycle change — status is filled, canceled, rejected, triggered, etc. |
| liquidation | lid, liquidator, liquidatedUser, liquidatedNtlPos, liquidatedAccountValue | Liquidation detected — only available via WebSocket (requires --no-ws to be absent) |
REST Polling Events
| Event | Payload | When |
|---|---|---|
| tick | timestamp, pollCount | Every poll cycle |
| funding_update | coin, fundingRate, annualized, premium | Every poll for all assets with registered handlers |
| position_opened | coin, side, size, entryPrice | New position detected |
| position_closed | coin, previousSize, entryPrice | Position no longer present |
| position_changed | coin, oldSize, newSize, entryPrice | Position size changed |
| pnl_threshold | coin, unrealizedPnl, changePct, positionValue | PnL moved > 5% of position value |
| margin_warning | marginUsedPct, equity, marginUsed | Margin 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
liquidationandorder_updateevents 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.
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 leverageconst 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 historyUtility 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.sqliteThe 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 1000Reports 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:
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
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| Flag | Description | Default |
|---|---|---|
| --dry | Intercept all write methods — no real trades placed | — |
| --verbose | Show debug output from the runtime | — |
| --id <name> | Custom automation ID (default: filename without .ts) | — |
| --poll <ms> | Poll interval in milliseconds | 30000 (ws on), 10000 (ws off) |
| --no-ws | Disable 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=value | Pre-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.