WakeTrades

API for AI Agents

Webhooks

Receive WakeTrades alerts as signed JSON on an HTTPS endpoint instead of Telegram. Link it with PUT /api/v1/webhook, verify the WakeTrades-Signature header, and the free trial starts.

A webhook is the alert channel for agents: WakeTrades sends each copy event (a position opened or closed, a stop hit, the key about to expire…) as a signed JSON POST to an HTTPS address the agent chooses. It replaces Telegram; both can be linked at the same time.

Through the API, a webhook or Telegram is required to start copying; an open stream doesn't count. No public HTTPS server? Have your human link Telegram instead: POST /api/v1/telegram/link returns a link they open in Telegram.

PUT /api/v1/webhook with { "url": "https://agent.example.com/waketrades" }

  • The address must be https, on port 443, with a public host name (no IP address, localhost, .local or .internal).
  • WakeTrades first sends a webhook.test event to it. Your endpoint must answer with a 2xx status within 5 seconds, or the link is refused (422 webhook_unreachable).
  • Activation step: this webhook.test is signed with the new secret, which you only receive in the answer to this same PUT. Your endpoint can't verify it yet: answer 2xx to an event of type webhook.test without checking its signature, then verify every other event. Never act on a webhook.test.
  • The answer holds the signing secret (whsec_…). It is shown once: store it. Linking again gives a new secret.
  • If the trading key is already signed, copying and the 14-day free trial start now ("state": "trial"). A new wallet whose webhook host already had a free trial gets none: the answer carries "noTrial": "webhook_host_used" and the state is trial_expired (pay to copy, Pay With USDC).

GET /api/v1/webhook shows the linked address (never the secret). DELETE /api/v1/webhook removes it; if no Telegram is linked either, copying pauses until a channel is linked again.

Events

{
  "id": "evt_mg8k2l1c_9f3a1b2c",
  "type": "open",
  "created": 1791633600000,
  "account": "0xYOUR_WALLET",
  "data": { "coin": "ETH", "side": 1, "sz": 0.4, "px": 2450.5, "notional": 980.2, "leader": "0xLEADER…" }
}
type When
open, add, cut, close A copy is opened, increased, reduced or closed (data: coin, side, size, price, notional, leader, pnl on reductions and closes, reason on closes)
stop_hit, tp_hit A stop or take profit of a copy was filled
manual You changed a copied position yourself on Hyperliquid
slots_full, limit_missed A leader's trade wasn't copied: your maximum of open positions is reached (Advanced), or the limit order expired
drawdown_alert Your account fell from its highest balance past your level's threshold, deposits and withdrawals excluded. data: { "level": "info" | "alert", "drawdownPct", "actions": ["close_losing", "close_all", "pause", "none"] }. Nothing happens unless you act: POST /api/v1/close-losing, /close-all or /pause
auto_stop Advanced only: your automatic stop was reached (drop from your highest balance, deposits and withdrawals excluded). Every copy is closed and copying is paused. data: { "drawdownPct", "stopPct", "equity", "peak" }; peak is your highest balance adjusted for deposits and withdrawals made since. Resume with POST /api/v1/resume
daily_loss_limit Advanced only: your balance fell past your daily loss limit (dailyLossPct) since its first reading of the UTC day, deposits and withdrawals excluded. No new copies until until (24 hours); open copies stay managed. data: { "equity", "dayStartEquity", "lossPct", "limitPct", "until", "netFlowUsd" }; equity is the balance read, netFlowUsd the deposits minus withdrawals since the day's first reading (USDC, plan payment excluded), and lossPct is computed on equity − netFlowUsd
account_blocked New copies are on hold at Prudent, Balanced or Risky. data: { "reason": "not_unified" | "manual_positions", "coins" }: switch with POST /account/unified, or close the positions opened outside WakeTrades (on Hyperliquid, MetaMask Perps or another app). Sent at most once a day
trial_started Copying and the 14-day free trial started. data: { "state": "trial", "trialEndsAt", "via": "webhook" | "telegram", "equity" }
plan_paid A plan payment was applied. data: { "planKind": "1m" | "3m" | "6m", "amount", "until", "state" } (until: the plan's new end, in ms)
account_unified WakeTrades switched your Hyperliquid account to a Unified Account (POST /account/unified, or the button). data: { "from", "mode" }
paused Copying paused: no new copies, open ones stay managed. data: { "state", "reason": "pause" | "close_all" | … } (close_all also covers the emergency stop and Advanced's automatic stop)
resumed Copying resumed. data: { "state" }
trial_j3, trial_j1, trial_end, plan_end Free trial or plan ending, then ended
plan_expiring Your paid plan ends soon: 30 days before the end of a 6-month plan, 14 days for 3 months, 7 days for 1 month, then the day before. data: { "days", "until", "planKind", "founderPrice" } (founderPrice: renewing before until keeps your founder price). Also in GET /api/v1/me as renewal while it applies
key_expiring, key_invalid The trading key ends in days days, or no longer works
close_failed, error A close or another order failed. Closes are retried for 24 hours, more and more spaced out
close_released A copy still couldn't be closed after 24 hours (no buyer or seller at a fair price). WakeTrades no longer manages it: close it yourself on Hyperliquid. Its stop stays in place
hip3_unified Stock and commodity perps need a unified Hyperliquid account
agent_linked, agent_unlinked An AI agent was connected to this wallet from the Profile, or disconnected. Sent to Telegram even when notifications are muted (Connect Your AI Agent)
agent_code_expiring Your agent's code from the Profile reaches its 180 days in 14 days, 7 days or tomorrow (days, until). Sent to Telegram even when notifications are muted. Copying is not affected.
announcement A message from the WakeTrades team to its clients (maintenance, incident, news). data: { "id", "text" }. Also shown on the dashboard until dismissed, and sent to Telegram even when notifications are muted
webhook.test Sent once when the webhook is linked

Trades skipped for another reason are not pushed; they are in GET /api/v1/orders and on the real-time stream, with a stable skip code (too_small, full, late, not_unified…; at Advanced also market_filtered, leverage_filtered, side_filtered, daily_loss_limit); reason is a free internal text that may change. With PUT /api/v1/notifications { "muted": true }, position moves are not pushed; action events and trial_started, plan_paid, account_unified, paused, resumed still are. These five go to the webhook, the real-time stream and GET /api/v1/journal, not to Telegram.

Retries and Duplicates

If your endpoint doesn't answer 2xx within 5 seconds, the event is sent again after about 1, 5 and 15 minutes, 1 hour, then every 3 hours, and dropped 24 hours after it happened. Each request carries WakeTrades-Event-Id, the same as the event's id on every retry: store the ids you processed and ignore a repeat. If your endpoint was down longer, catch up with GET /api/v1/journal. To get every event and your leaders' moves live, open the real-time stream.

Before each delivery, WakeTrades checks where your host name points: an address that resolves to a private network is never called.

Verify the Signature

Each request carries WakeTrades-Signature: t=<timestamp in ms>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> with your secret. Reject requests whose signature doesn't match or whose t is more than 5 minutes old.

Python

import hmac, hashlib, time

def verify(secret: str, header: str, raw_body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() * 1000 - int(t)) > 5 * 60 * 1000:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

JavaScript (Node)

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1 || Math.abs(Date.now() - Number(parts.t)) > 5 * 60_000) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Last updated October 28, 2026