Alerts
Alerts the user sets on prices, spreads and games, the alerts list, and the notification history (/alerts and /notifications). Built in Phase 4 against the contract in
api/openapi.yaml (Alert, CreateAlertRequest, UpdateAlertRequest, Notification).
How a fired alert is delivered (toast, desktop, sound, Telegram) is in
notifications.md.
Wireframe notation (README.md): [name] in lower case is a lucide icon, [Label] is a
button, [ ] is a checkbox, (dot) is a 6 px status dot.
Alert kinds
Trigger rules
These come from the contract; the UI states them and never works around them.- Above uses the best bid, below uses the best ask. The form labels say so (
Bid at or above,Ask at or below), so the user knows which side of the book is watched. - Thresholds sit on the market’s tick. The price input steps by the tick and the backend
refuses off-tick prices (
validation_failed). Spread thresholds are in cents too. - Repeat. Off: the alert fires once and becomes
triggered. On: it fires, then re-arms once the value moves one tick back across the threshold (a bid alert at 60¢ re-arms when the bid falls to 59¢). Repeat applies to price, spread and score alerts; game start and game end fire once by nature, so the form hides Repeat for them. - Stale feeds never fire. A price or spread alert does not fire while its book is stale, and a game alert does not fire while the sports feed is stale. The list greys the current value; nothing is filled from another source.
- Restart. Alerts persist and resume from the live books after a backend restart. They do not fire on the first snapshot unless the condition newly holds.
- Limit. At most 200 alerts exist at once, in any status.
- Alerts work in Safe, with trading off and while geoblocked: they place no orders.
Condition text
One format everywhere: the list, the create toast, the ladder tooltip and the notification body (notifications.md).{outcome} is the outcome name. For Yes and No outcomes it is {market} · {outcome}
(Fed decision December · Yes), with the market title truncated. The market or game title
(the alert’s title) shows in its own column or line, never inside the condition.
Creating an alert
Every entry point prefills the form, so the common case is one click. Creating works in Safe and with trading off.From a ladder level (one click)
- The
[bell]shows only on the hovered row (or the keyboard-cursor row), at the left edge of the P&L cell: a 16 × 16 icon button holding the 16 px icon with no padding,--muted-foreground, on the row’s hover background. A 20 px button overlapped the P&L value in narrow ladders, so the button is the icon’s size. The P&L value keeps its place on the right of the cell; the bell is absolutely positioned and takes no width from it. On touch the marker is 32 px with a 44 px hit area. - Tooltip and accessible name:
Set alert at 42¢. - Defaults:
alert_kind: price, the ladder’s outcome,price= the level,repeat: false, and the direction from the level’s side of the book:- level above the midpoint:
above(fires when the bid rises to it); - level at or below the midpoint:
below(fires when the ask falls to it). So the condition never holds at the moment it is set. With one side of the book empty, the last trade stands in for the midpoint; with no book, the bell is hidden.
- level above the midpoint:
- The toast reads
Alert set · Lakers bid at or above 60¢withUndo(5 s). Undo deletes the alert. - Level marker. A level with an active or paused alert shows the
[bell]permanently in the same place (--muted-foreground; paused usesbell-off). Clicking it opens the edit form. Tooltip:Edit alert · Lakers bid at or above 60¢. A triggered one-shot alert shows no marker. The space is always reserved, so a marker never moves the row. - A level with two alerts (above and below) shows one marker; clicking it opens the first,
and the form lists the other as
1 more alert at this pricewith a link to the list. - Mobile. The ladder sheet has no hover, so levels carry no bell until an alert exists.
Long-press a level opens the action sheet:
Set alert at 42¢is its first item. Markers show as on desktop and a tap on the marker opens the edit sheet.
From the board (game alerts)
- The game’s menu gets three checkbox items after
Pin, separated by a divider:Alert on score change,Alert at game start,Alert at game end. - Choosing an unchecked item creates that alert (
repeat: truefor score change, since it exists to follow the game) and showsAlert set · Score change · Lakers v CelticswithUndo. Choosing a checked item deletes it, with the same undo toast as the list. Alert at game startis disabled once the game is live (tooltipGame already live), and every item is disabled when the game has ended (Game ended).- Desktop: the menu opens from right-click on the game block and from an
[ellipsis]icon button that appears on hover or focus at the right end of the game’s first line (in the+Ncolumn). Mobile: long-press the card, or the[ellipsis]in the card header, which touch keeps visible.
From the market view
- The
[bell]icon button in the market header opens the form, prefilled with the selected outcome; on mobile it isSet alert…in the[ellipsis]menu. - The price chart beside it labels its time axis in local 24-hour time (
14:05; the day of the month,Sepor2026at day, month and year boundaries), the same clock as the Triggered and Set columns, so a triggered alert can be found on the chart. A label the plot’s edge would cut is left out rather than drawn in half. The same axis rule applies to the P&L chart on the portfolio.
The form (edit, and create from the market view or this screen)
A popover on desktop (360 px, anchored to the control that opened it), a bottom sheet on mobile. Controls are--control-h (32 px) on desktop and 44 px on touch, one per row,
labels in a 72 px column on desktop and above the control on mobile.
- Type:
Price,Spread,Score change,Game start,Game end. The game types are offered only when the market belongs to a game. - When (price and spread): price shows
Bid at or above/Ask at or below; spread showsAt or above/At or below. Game types hide When, Price and Outcome. - Price: a tick stepper and a typed input, in cents.
now 43¢shows the value the condition watches (the bid for above, the ask for below, the spread for spread alerts), muted, and greys with the book when stale. - Repeat: a checkbox labelled
After one tick back(the re-arm rule). Hidden for game start and game end. - Footer: create shows one primary button,
Set alert. Edit is titledEdit alertand showsDelete(ghost, left) andSave(primary, right). Esc and[x]close without saving; there is no Cancel button (Cancel is reserved for orders). - Validation under the field in
--destructivebody-sm:Price must be on the 1¢ tick,Price must be between 1¢ and 99¢. The server’s 400 message maps to the same lines. - Submit: the button shows its loading state; on success the popover closes and the
toast
Alert set · {condition}appears. On failure the popover stays with the error line reserved under the footer (20 px), e.g.Not set · 200 alerts, the maximum.
Create and update errors
Alerts list (desktop, 1376 px content)
Route/alerts, document.title Alerts · sesame. One panel on the page surface: a
header bar, a divider, the table. No cards, no nested boxes.
Header (48 px)
- 48 px, the same height as the Orders and Portfolio headers, so the three screens line up when switching between them. 16 px side padding, a divider under it.
- Left: the tab strip
Alerts/Notifications, sitting on the surface (no filled group).Notificationscarries the unread count badge (the rail’s count badge component) when unread > 0. - Right:
Set alert…(primary,--control-h; the only filled button on the screen), vertically centred. Disabled at 200 alerts with the tooltip200 alerts, the maximum. It shows only on the Alerts tab.
Columns
- Rows are 36 px,
--borderdivider between rows, hover--accent. Sorted newest first (the contract’s order). Structure changes defer while the list is in use (interactions.md, “Deferred list changes”). - Price alerts in the list subscribe the book topics of their visible rows, as every list does (§3.7); the Now value is display-only.
- Row click (not on a control) opens the edit popover anchored to the row.
- A triggered alert shows
Resume(makes it active again, per the contract) andDelete.
Row actions
- Undo on delete. The DELETE is held for the 5 s of the toast.
Undoputs the row back with its status unchanged and no request is sent. When the toast closes (timeout or[x]), the DELETE is sent; if it fails, the row returns and the toast readsAlert not deleted · {reason}. If the tab closes within the 5 s, the alert stays; that is the safe side. No confirm dialog. - Several deletes in a row each get their own toast (stacking rules in notifications.md).
Keys (navigation class, work in Safe)
States (alerts list and notification history)
Each state has the loaded dimensions: header, column headers and 36 px row slots stay.Notification history
Route/notifications, the second tab of the Alerts screen, document.title
Notifications · sesame. Every notification is kept here for 30 days, whatever the
channel rules say (in_app only controls the toast).
The tab shows the same list component as the desktop top-bar [inbox] popover and the
phone’s notifications sheet (NotificationHistory in app/shell/notifications-panel.tsx),
so the history looks and behaves the same wherever it opens. It is not a separate
day-grouped table. On desktop the list sits in a centred column at most 768 px wide under
the 48 px header; on the phone it fills the width.
Unread count
unread_countcomes with thenotificationstopic snapshot; each delta adds one while the history is not open.- It shows on the desktop top bar’s
[inbox]button, on theNotificationstab, and on the phone’s logo menu button and its notifications item (3 unread): the count badge, 16 px,--secondary, never red or green. Above 99 it reads99+. - Unread notifications are the only count; triggered alerts have no separate count.
Marking read
- Opening the history (the tab, the popover or the sheet) sends
POST /notifications/readwithup_to= the newest loaded notification’s id, once the first page has loaded and anything is unread. The server re-sends the snapshot, so the count clears in every tab. On failure the count stays and the next change tries again. - New notifications that arrive while the history is open are marked read the same way.
- There is no
Mark all readbutton: opening the history is the action. - Rows that were unread when the history opened, and rows that arrive while it is open, keep the unread style until it closes, so the new ones stay recognisable.
Rows
- Rows have dividers, 16 px side padding and 8 px vertical padding; minimum height 56 px, 64 px on touch. Newest first.
- Unread marker: a 6 px
--ringdot at the left, in a slot every row reserves, so the text never moves when it clears. Unread titles are weight 600 and carry the screen-reader prefixUnread:. - Title line: the
title, truncated with the full text in a tooltip, and the time on the right (2m ago, then30 Sep 14:05, 24-hour local; exact time in a tooltip). A critical notification’s title is--destructive; info and warning titles are--foreground. Severity has no other mark in the list. - Body: 13 px,
--muted-foreground, clamped to two lines with the full text in a tooltip.notifykeeps bodies within 120 characters (notifications.md), so the figures are rarely cut. - Rows with a target are links: a click opens it (notifications.md, “Targets”) and, in the popover or sheet, closes that overlay. A row without a target is not interactive and has no hover.
- Paging: 50 per page.
Load older(ghost,--control-h) under the last row fetches the next page withbefore=next_before; it shows its loading state and is gone on the last page. New notifications from the topic insert at the top.
Alerts list, mobile (390 px)
Reached from the logo menu (Alerts, with the unread count) or > alerts in the palette.
A full-screen route: the app header stays, with a back button in place of the logo.
- Rows are 64 px, separated by dividers, not cards. Line 1: condition (body), line 2:
market and the watched value (body-sm, muted). Status (
Active,Paused, orTriggered 2m ago · 54¢, which folds in the desktop Triggered column; then· Repeatwhen on) sits right-aligned on line 2; the value never truncates, the market does. - The
[ellipsis]button is always visible on touch and opens the action sheet:Edit,PauseorResume,Delete. Long-press the row opens the same sheet. Tap the row: edit sheet. Set alert…opens the form as a bottom sheet, with the market search field first.- Delete uses the same undo toast (bottom of the screen, above the tab bar).