apexPlayerAlert — context for future edits

Monitors Apex Manager auctions (https://apexmanager.rootsxo.studio/api/auctions, auth Authorization: Bearer <APEX_TOKEN>) every 15 min and pushes alerts to ntfy (<NTFY_TOPIC>) and Discord (<DISCORD_WEBHOOK_URL>). Alerts one per auction via rolling 24h dedupe (blob key apex_cheap_player_alert_seen_ids stores {auctionId: firstSeenTs}).

Alert rules (all in matchRule, labels joined with +)

  1. prodigy (★ Prodigy B+ pot)growthClass is prodigy, displayed potential is B+, AND salary < PRODIGY_MAX_SALARY (3500). Data proves prodigy + B+ potential carries a 60.0% S-tier conversion rate (vs. ~3.0% for all other growth classes).
  2. practice_partner (Cheap Practice Partner) — archetype is strictly Practice Partner (normalized, case/space insensitive) AND salary < 2500.
  3. bplus_high_fog (B+/B+ high fog) — displayed rank is B+, displayed potential is B+, intel < GEM_MAX_INTEL (0.35, meaning Fog > 65%), AND salary < GEM_MAX_SALARY (4100). High floor + high upside lottery.
  4. cplus_100_fog (C+/B+ 100% fog) — displayed rank is C+, displayed potential is B+, fog is 100% (fogConfidence = 0), AND salary < GEM_MAX_SALARY (4100).

Salary thresholds

  • Per-rule salary caps live in named constants at the top of main.ts: PRODIGY_MAX_SALARY = 3500 (rule 1), ARCHETYPE_MAX_SALARY = 2500 (rule 2), and GEM_MAX_SALARY = 4100.
  • Rules 3 and 4 share the single GEM_MAX_SALARY constant — editing it moves both thresholds at once. Split it into two constants if they ever need to differ.
  • Edge case: salary is read as p.salary ?? Infinity, so a listing with no salary value fails every < threshold check and will not fire rules 1/3/4 (rule 2 already required a salary). A missing salary is effectively treated as "too expensive," not a match. In practice listings always carry a salary, so this only matters for malformed/edge data.

Notification contents

  • Player role (Support / IGL / Fragger) is on every alert: rendered as [Role] after the name in both the ntfy title and the Discord embed title, plus a dedicated Role field in the Discord embed. Sourced from p.role and shown as h.role ?? "?" so a missing role degrades gracefully.

⚠️ CRITICAL fog & tier mechanics (scouting data findings)

  1. Variance Ceiling Cap (+5 / +6 Max):

    • Upward tier roll is hard-capped by fog decile:
      • 0%–30% Fog: 0 tiers (exact match).
      • 40%–50% Fog: $\le 3$ tiers max (B+ cannot reach S-tier; maxes at A).
      • 56%–79% Fog: $\le 5$ tiers max (B+ can reach S-tier).
      • 80%–100% Fog: $\le 6$ tiers max (B+ can reach S+).
    • Across 2,000+ retired player resolutions, 100% of all S-tier hits came from displayed B+ potential. Zero came from B, B-, or lower.
  2. The Prodigy Multiplier:

    • growth_class = 'prodigy' is the primary discriminator on the market.
    • Prodigy + B+ potential resolves into S-tier at 60.0%.
    • Novice, Amateur, and Veteran with B+ potential resolve into S-tier at only $1.9%\text{–}4.7%$.
  3. Intel Semantics:

    • fogConfidence is INTEL (scouting confidence), NOT fog obscuring.
    • Game "Fog %" = 100 − fogConfidence · 100 (e.g. fogConfidence = 1.0 is 0% fog; 0.0 is 100% fog).
    • High intel = confirmed true ceiling.
    • Low intel = wide distribution / lottery.
  4. Trial Caps (attributeCaps) are Fogged:

    • After a trial, statsRevealed: true appears, but attributes and attributeCaps are provisional and fluctuate as intel rises. They can trend downwards upon retirement. Do not treat trial caps as confirmed true ceilings.

Data Schema & API Endpoints

  • Live Market Feed: /api/auctions provides endAt ISO timestamps for calculating real-time auction expiry and countdown timers.
  • True Ceiling Discovery: trueRating and potentialRating are only populated once a player retires (available in /api/watchlist and /api/players/:id).
  • Rating Scale: Linear 15 grades (D- to S+) over 0–3000 rating points (~200 pts per grade).

Env vars

  • APEX_TOKEN — API bearer token (required)
  • NTFY_TOPIC — ntfy.sh topic to post to (required)
  • DISCORD_WEBHOOK_URL — Discord channel webhook URL (optional/required)