> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sesameterminal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Alerts

# 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](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

| `alert_kind` | Name in the UI | Fires when | Target |
| - | - | - | - |
| `price`, `cross_direction: above` | Price alert | The best bid reaches the threshold or higher (the outcome can be sold there) | `token_id` |
| `price`, `cross_direction: below` | Price alert | The best ask reaches the threshold or lower (the outcome can be bought there) | `token_id` |
| `spread`, `above` | Spread alert | Ask minus bid widens to the threshold or more | `token_id` |
| `spread`, `below` | Spread alert | Ask minus bid narrows to the threshold or less | `token_id` |
| `score` | Score change | Each change of the game's score | `game_id` |
| `game_start` | Game start | The game goes live | `game_id` |
| `game_end` | Game end | The game ends | `game_id` |

### 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](notifications.md#copy-per-notification-kind)).

| Kind | Condition text | Example |
| - | - | - |
| Price, above | `{outcome} bid at or above {price}` | `Lakers bid at or above 60¢` |
| Price, below | `{outcome} ask at or below {price}` | `Lakers ask at or below 40¢` |
| Spread, above | `{outcome} spread at or above {price}` | `Lakers spread at or above 4¢` |
| Spread, below | `{outcome} spread at or below {price}` | `Lakers spread at or below 1¢` |
| Score | `Score change` | `Score change` |
| Game start | `Game start` | `Game start` |
| Game end | `Game end` | `Game end` |

`{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.

| Entry point | Desktop | Mobile | Result |
| - | - | - | - |
| Ladder level | Hover a level: a `[bell]` icon button appears in that row's P\&L cell. Click it | Long-press a level: action sheet item `Set alert at 42¢` | Creates at once with the defaults below; toast with `Undo` |
| Ladder level, keyboard | `Alt+Enter` at the keyboard cursor, or Alt+click a Bid or Ask cell | none | Same |
| Board game | Row menu (right-click, or the hover-revealed `[ellipsis]`): `Alert on score change`, `Alert at game start`, `Alert at game end` | Long-press the card, or the card's `[ellipsis]`: same items | Creates at once; toast with `Undo` |
| Market view | `[bell]` in the header (tooltip `Set alert…`) | `[ellipsis]` menu item `Set alert…` | Opens the form, prefilled with the selected outcome |
| Alerts screen | `Set alert…` in the header | Same | Opens the form with a market search field |

### 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.
* The toast reads `Alert set · Lakers bid at or above 60¢` with `Undo` (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 uses `bell-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 price` with 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: true` for score change, since it
  exists to follow the game) and shows `Alert set · Score change · Lakers v Celtics` with
  `Undo`. Choosing a checked item deletes it, with the same undo toast as the list.
* `Alert at game start` is disabled once the game is live (tooltip `Game 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
  `+N` column). 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 is `Set 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, `Sep` or `2026` at 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.

```
 ┌────────────────────────────────────────────────┐
 │ Set alert                                  [x] │ 44 title
 │ Market   [Lakers v Celtics · ML              ] │ 32 search field (only from this screen)
 │ Type     [Price                [chevron-down]] │ 32 select
 │ Outcome  [ Lakers        | Celtics           ] │ 32 segmented
 │ When     [ Bid at or above | Ask at or below ] │ 32 segmented
 │ Price    [−] [   60¢   ] [+]   now 43¢         │ 32 tick stepper, current value
 │ Repeat   [ ] After one tick back               │ 32 checkbox
 │ Note     [                                   ] │ 32 text, 200 characters
 │                                    [Set alert] │ 44 footer, primary
 └────────────────────────────────────────────────┘
```

Every control spans the same 264 px field column, so the segments of one control share
its width equally.

* **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
  shows `At 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 titled `Edit alert`
  and shows `Delete` (ghost, left) and `Save` (primary, right). Esc and `[x]` close without
  saving; there is no Cancel button (Cancel is reserved for orders).
* **Validation** under the field in `--destructive` body-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

| Response | Text (toast for one-click, form line otherwise) |
| - | - |
| 429 `limit_exceeded` | `Alert not set · 200 alerts, the maximum` |
| 404 `not_found` | `Alert not set · market not found` |
| 400 `validation_failed` | `Alert not set · {field message}` |
| Network or 5xx | `Alert not set · no response from server` |
| Pause, resume or save failed | `Alert not paused · {reason}`, `Alert not resumed · {reason}`, `Alert not saved · {reason}` |

## 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.

```
 x: 64                                                                                          1440
 ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
 │ Alerts   Notifications 3                                                       [Set alert…]  │ 48 header
 ├──────────────────────────────────────────────────────────────────────────────────────────────┤
 │ Status   Condition                      Market                  Now  Triggered      Set      │ 32 column headers
 │ Active   Lakers bid at or above 60¢     Lakers v Celtics · ML   43¢                 2m ago   │ 36
 │ Active Repeat  Lakers spread at or above 4¢  Lakers v Celt…     2¢   1h ago · 5¢    5h ago   │ 36
 │ Paused   Score change                   Lakers v Celtics  Q3 4:12 88–84             1h ago   │ 36
 │ Triggered  Fed decisi… · Yes ask at or below 55¢  Fed deci…     54¢  2m ago · 54¢   30 Sep   │ 36
 └──────────────────────────────────────────────────────────────────────────────────────────────┘
   hovered row: every cell stays; [Pause] [Delete] appear in the 168 px actions slot
```

### 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).
  `Notifications` carries 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 tooltip `200 alerts, the maximum`.
  It shows only on the Alerts tab.

### Columns

| Column | Width | Content |
| - | - | - |
| Status | 112 | `Active`, `Paused` or `Triggered` (the last two `--muted-foreground`), then `Repeat` in muted body-sm when `repeat` is on. Words only, no coloured dots: status is quiet |
| Condition | flex (min 200) | The condition text. When the row has no free space, only the market part of a `Yes`/`No` condition truncates (`Fed decisi… · Yes ask at or below 55¢`); the side and price never do. Full text in a tooltip, with the note if there is one |
| Market | flex | The alert's `title`, muted. Truncates like Condition. Click: open the market view |
| Now | 128 | The watched value: bid for above, ask for below, spread for spread alerts, the compact score badge for game alerts. Live, no flash, never truncated. Greyed when its feed is stale, tooltip `Stale 6s · the alert does not fire while stale` |
| Triggered | 136 | When the alert last triggered and, for price and spread alerts, the value that triggered it (`triggered_value`): `2m ago · 54¢`, then `30 Sep 14:05 · 54¢`. Game alerts show the time only. Blank until it first triggers; a repeating alert keeps its last time here while its status stays `Active`. Muted, exact local time in a tooltip |
| Set | 88 | `created_at` relative (`2m ago`), then `30 Sep 14:05`; exact local time in a tooltip |
| Actions | 168 | Empty until row hover or keyboard focus, then `Pause` or `Resume` and `Delete`, ghost text buttons at `--control-h`. Reserved width, so nothing shifts |

* Rows are 36 px, `--border` divider 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) and `Delete`.

### Row actions

| Action | Request | Feedback |
| - | - | - |
| `Pause` | `PUT /alerts/{id}` `{paused: true}` | Button shows its loading state; the row updates from the `alerts` WS delta |
| `Resume` | `PUT /alerts/{id}` `{paused: false}` | Same |
| `Delete` | `DELETE /alerts/{id}`, sent when the undo toast closes | The row leaves at once (no ghost row: the user removed it). Toast `Alert deleted · Lakers bid at or above 60¢` with `Undo`, 5 s |

* **Undo on delete.** The DELETE is held for the 5 s of the toast. `Undo` puts 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 reads
  `Alert 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)

| Keys | Action |
| - | - |
| `↑` / `↓` | Move the row cursor |
| `Enter` | Edit the alert under the cursor |
| `P` | Pause or resume it |
| `Delete`, `Backspace` | Delete it (undo toast) |
| `g` then `a` / `n` | Go to Alerts / Notifications |

## States (alerts list and notification history)

Each state has the loaded dimensions: header, column headers and 36 px row slots stay.

| State | Alerts | Notifications |
| - | - | - |
| Loading (cold) | Skeleton: 8 rows with the real column widths, static `--muted` blocks | Skeleton: 5 rows, a title bar and a body bar each |
| Empty | `No alerts` and one line `Set one with the bell on a ladder price or from a game’s menu`. No extra action (the header has `Set alert…`) | `No notifications in the last 30 days` |
| Error | `Alerts not loaded · {reason}` and `Retry` (ghost) | `History unavailable · {reason}` and `Retry`. When rows are already loaded and an older page fails: `Older history unavailable · {reason}` under the last row |
| Stale (the `alerts` or `notifications` topic lost) | Rows stay, Now values grey, the header shows the small `Stale 6s` pill. Pause, Resume and Delete still work (they are REST calls) | Rows stay and dim; the line `Stale · new notifications paused` sits above the list until the socket resubscribes |
| Book or game feed stale for one row | That row's Now cell greys with the tooltip above | none |
| At the limit | `Set alert…` disabled, tooltip `200 alerts, the maximum`; ladder bells stay and a click gives the 429 toast | none |

## 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_count` comes with the `notifications` topic snapshot; each delta adds one while
  the history is not open.
* It shows on the desktop top bar's `[inbox]` button, on the `Notifications` tab, 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 reads `99+`.
* 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/read`
  with `up_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 read` button: 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

```
 ┌────────────────────────────────────────────────────────┐
 │ (dot) Order filled                              2m ago │ 56 min: title line, time on the right
 │       Lakers · Buy 25 sh at 42¢                        │    body, muted, up to 2 lines
 ├────────────────────────────────────────────────────────┤
 │       Price alert                              14m ago │
 │       Lakers bid 61¢ · at or above 60¢                 │
 ├────────────────────────────────────────────────────────┤
 │ (dot) Feed disconnected                         1h ago │ critical: title in --destructive
 │       Polymarket market data · no connection for 30s   │
 ├────────────────────────────────────────────────────────┤
 │                     [Load older]                       │ only when there is an older page
 └────────────────────────────────────────────────────────┘
```

* 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 `--ring` dot 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
  prefix `Unread:`.
* **Title line:** the `title`, truncated with the full text in a tooltip, and the time on
  the right (`2m ago`, then `30 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. `notify` keeps 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 with `before` = `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.

```
 ┌──────────────────────────────────────┐
 │ Alerts   Notifications 3 [Set alert…]│ 44 tab row, primary on the right
 ├──────────────────────────────────────┤
 │ Lakers bid at or above 60¢ [ellipsis]│ 64 line 1: condition, overflow button (44 × 44)
 │ Lakers v Celtics · ML · 43¢   Active │    line 2: market, now, status
 ├──────────────────────────────────────┤
 │ Score change               [ellipsis]│ 64
 │ Lakers v Celtics · Q3 4:12 88–84     │
 │                               Paused │
 └──────────────────────────────────────┘
```

* 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`, or
  `Triggered 2m ago · 54¢`, which folds in the desktop Triggered column; then `· Repeat`
  when 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`,
  `Pause` or `Resume`, `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).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.