Alert rules live under
/v1, not /v2. Responses are the resource itself (AlertRule, AlertRuleList, …), not wrapped in the {data, meta} envelope that /v2 endpoints use.What you build
A rule that fires on APY drop, plus the list/update/pause/delete/audit operations you need once it’s live.1. Create a rule
Creating and modifying rules requires thewrite:alerts scope (which also grants read:alerts — see Scopes).
data_source is one of yield_radar, hyperliquid, watchlist, vault_events, curator_events, oracle_events, and it cannot be changed after creation. conditions is evaluated as AND — every condition must match for the rule to fire. channel_ids is optional and omitted here: an empty or absent list means in-app notification only. To fan out to email or webhook channels, create those channels first in the dashboard under Settings → Notifications, then pass their IDs — channel management itself is not part of this API.
A 404 on create means one or more channel_ids don’t belong to you; a 409 means a rule with that name already exists; a 400 means the conditions or noise-control fields (cooldown_minutes, require_persistence_minutes, dedupe_window_minutes, quiet_hours_*) failed validation.
2. List and inspect rules
enabled is an optional query filter on the list endpoint; omit it to get every rule regardless of state.
3. Update a rule — partial-merge PUT
PUT /v1/alerts/rules/{id} is a partial merge, not a full replace: only the fields present in the body change, and everything you omit keeps its current value. data_source is never accepted here.
The trap: “present” means present in the JSON, not “non-empty”. If you send "channel_ids": [], that’s an explicit instruction to clear the channels — different from leaving channel_ids out entirely, which leaves them untouched. The same applies to conditions: there’s no per-condition merge, so sending conditions replaces the whole array. To add one condition to an existing rule, resend all of its existing conditions plus the new one — send just the new one and you silently drop the rest.
curl
4. Pause or resume without touching the definition
Toggling doesn’t count as an update to the rule’s conditions, channels, or template — use it for a temporary pause instead of re-sending the full rule.curl
python
5. Delete a rule
curl
204 with no body. Deleting is permanent — there’s no undo endpoint; recreate the rule if you delete it by mistake.
6. Read the fire history
GET /v1/alerts/events returns your rule firings, most recent first — useful for confirming a rule actually fired, or for building your own audit log.
data_snapshot (the observation that triggered the rule), status (sent, suppressed, or failed), and a dispatches array with one entry per channel — each with its own status, sent_at, and error_message if delivery failed. Filter by status, or by start_date/end_date (RFC3339) to bound a window; limit maxes out at 1000.
7. Troubleshooting
403 insufficient_scopeon read-only calls: list/get on rules and events only needread:alerts; awrite:alertskey also has it, but aread:alerts-only key can’t create, update, toggle, or delete.- A
PUTdidn’t change what you expected: re-read step 3 — you likely sent a field that overwrote something you meant to leave alone (or omitted one you meant to change). - Rule doesn’t fire: check
enabled(toggle may have flipped it), confirmcooldown_minutesorquiet_hours_*isn’t currently suppressing it, and pull/v1/alerts/events?rule_id=...&status=suppressedto see suppressed-not-silent fires. - Rule fires but you get no notification: check the event’s
dispatches[].status— a channel-side failure (bad webhook URL, bounced email) shows there witherror_message, even though the rule itself fired.