koala73/worldmonitor · error · ConvexError

TICKERS_LIMIT_EXCEEDED

TICKERS_LIMIT_EXCEEDED

Error message

tickers list capped at ${TICKERS_MAX} entries

What it means

normalizeTickers (convex/alertRules.ts:160) applies the same pattern as countries to watchlist ticker-scope (#4922 U3): trim, uppercase, keep only shapes matching ^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$ (AAPL, BRK-B, RELIANCE.NS), silently drop others (^GSPC, GC=F, cashtags, prose), dedupe, then enforce TICKERS_MAX = 50. More than 50 distinct valid ticker shapes throws ConvexError { code: 'TICKERS_LIMIT_EXCEEDED', message: 'tickers list capped at 50 entries' }.

Solutions

  1. Normalize client-side with the same regex and dedupe, then cap at 50 before calling.
  2. Limit ticker-scoped alerts to the user's top-50 watchlist entries — the cap mirrors the market-watchlist client cap by design.
  3. For imports, validate and trim beforehand: the mutation rejects atomically, no partial write happens.
  4. If a higher cap is genuinely needed, raise TICKERS_MAX in convex/alertRules.ts together with the client watchlist cap so both stay in sync.

Example fix

// before
await mutateAPI.alertRules.setAlertRules({ variant: 'default', enabled: true, eventTypes: ['markets'], channels: ['email'], tickers: fullPortfolio }); // 120 valid symbols
// ConvexError: TICKERS_LIMIT_EXCEEDED

// after — same shape filter + dedupe + cap as the server
const TICKER_RE = /^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$/;
const normalizeTickers = (input: string[]) =>
  [...new Set(input.map((t) => t.trim().toUpperCase()).filter((t) => TICKER_RE.test(t)))].slice(0, 50);
await mutateAPI.alertRules.setAlertRules({ variant: 'default', enabled: true, eventTypes: ['markets'], channels: ['email'], tickers: normalizeTickers(fullPortfolio) });
Defensive patterns

Strategy: validation

Validate before calling

// Replicate normalizeTickers, then cap at 50.
const TICKER_RE = /^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$/;
function normalizeTickers(input: string[]): string[] {
  return [...new Set(input.map((t) => t.trim().toUpperCase()).filter((t) => TICKER_RE.test(t)))].slice(0, 50);
}
await mutateAPI.alertRules.setAlertRules({ ...args, tickers: normalizeTickers(selectedTickers) });

Type guard

const isWithinTickersCap = (v: unknown): v is string[] =>
  Array.isArray(v) &&
  new Set((v as string[]).map((t) => String(t).trim().toUpperCase()).filter((t) => /^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$/.test(t))).size <= 50;

Try / catch

try {
  await mutateAPI.alertRules.setAlertRules(args);
} catch (err) {
  if (err instanceof ConvexError && (err.data as { code?: string }).code === 'TICKERS_LIMIT_EXCEEDED') {
    setPickerError('Pick at most 50 tickers');
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: setAlertRules (or setAlertRulesForUser / setNotificationConfigForUser) with a tickers array holding more than 50 distinct shape-valid symbols after normalization — e.g. importing a full portfolio or a watchlist that grew past the client cap.

Common situations: Client watchlist code (src/services/market-watchlist.ts caps at 50) bypassed by bulk import or direct API use; portfolio CSV imports with hundreds of symbols; scripts replaying watchlists into alert rules; users assuming indices/futures count — dropped shapes do not count toward the cap, only valid ones do.

Related errors


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

Appendix: 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 eeab0a219f)