koala73/worldmonitor · error · ConvexError

TICKERS_LIMIT_EXCEEDED

TICKERS_LIMIT_EXCEEDED

Error message

tickers list capped at ${TICKERS_MAX} entries

What it means

Thrown by normalizeTickers when the cleaned/deduped ticker list exceeds TICKERS_MAX (50). Input is trimmed, uppercased, filtered to ^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$ (plain symbols, share-class, dot-suffix exchange listings), and deduped before the cap check. The cap mirrors the client-side market watchlist limit in src/services/market-watchlist.ts.

Source

Thrown at convex/alertRules.ts:172

 *  - cap at TICKERS_MAX
 *
 * NOT a registry check against shared/stocks.json — a shape-valid symbol
 * the extractor never emits simply never intersects at the relay. Keeps
 * alertRules independently shippable, same rationale as countries.
 */
function normalizeTickers(input: string[]): string[] {
  const cleaned: string[] = [];
  const seen = new Set<string>();
  for (const raw of input) {
    if (typeof raw !== "string") continue;
    const upper = raw.trim().toUpperCase();
    if (!/^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$/.test(upper)) continue;
    if (seen.has(upper)) continue;
    seen.add(upper);
    cleaned.push(upper);
  }
  if (cleaned.length > TICKERS_MAX) {
    throw new ConvexError({
      code: "TICKERS_LIMIT_EXCEEDED",
      message: `tickers list capped at ${TICKERS_MAX} entries`,
    });
  }
  return cleaned;
}

export const getAlertRules = query({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return [];
    return await ctx.db
      .query("alertRules")
      .withIndex("by_user", (q) => q.eq("userId", identity.subject))
      .collect();
  },
});

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Trim the tickers array to at most 50 unique valid symbols before submitting.
  2. Filter client-side to the regex ^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$ — invalid shapes (^GSPC, GC=F, cashtags) are silently dropped server-side anyway.
  3. Reconcile against src/services/market-watchlist.ts which enforces the same 50-entry cap on the client.

Example fix

// before
const tickers = portfolio.holdings.map(h => h.symbol); // 80 symbols
await setAlertRules(ctx, { ..., tickers });
// after
const tickers = portfolio.holdings
  .map(h => h.symbol.toUpperCase().trim())
  .filter((s, i, arr) => /^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$/.test(s) && arr.indexOf(s) === i)
  .slice(0, 50);
await setAlertRules(ctx, { ..., tickers });
Defensive patterns

Strategy: validation

Validate before calling

const TICKERS_MAX = 50;
const TICKER_RE = /^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$/;
function normalizeAndCapTickers(input) {
  const seen = new Set();
  const out = [];
  for (const raw of input) {
    const upper = String(raw).trim().toUpperCase();
    if (!TICKER_RE.test(upper) || seen.has(upper)) continue;
    seen.add(upper);
    out.push(upper);
    if (out.length === TICKERS_MAX) break;
  }
  return out;
}

Type guard

const TICKER_RE = /^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$/;
function isTickerList(input): input is string[] {
  return Array.isArray(input) && input.every(t => typeof t === 'string' && TICKER_RE.test(t.trim().toUpperCase()));
}

Try / catch

try {
  await setAlertRules(args);
} catch (e) {
  if (e instanceof ConvexError && /tickers list capped/.test(String(e.message))) {
    // truncate to top 50 and retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling setAlertRules, setAlertRulesForUser, or setNotificationConfigForUser with a tickers array yielding more than 50 unique valid symbols after normalization.

Common situations: Bulk import from a portfolio CSV; a watchlist sync that copies an entire index membership; a patched client bypassing the UI cap.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/5e11cfbb8bf40e60. Report an issue: GitHub.