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
- Normalize client-side with the same regex and dedupe, then cap at 50 before calling.
- Limit ticker-scoped alerts to the user's top-50 watchlist entries — the cap mirrors the market-watchlist client cap by design.
- For imports, validate and trim beforehand: the mutation rejects atomically, no partial write happens.
- 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
- Dedupe and shape-filter before counting — indices, futures, and cashtags are dropped server-side and don't count.
- Cap ticker multi-selects at 50, mirroring the market-watchlist client cap.
- Validate imported portfolios before submission; the mutation rejects atomically with no partial write.
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
- COUNTRIES_LIMIT_EXCEEDED
- COMPANY_LIMIT_REACHED
- COMPANY_MONITORING_ADMISSION_QUERY_VERSION_INVALID
- COMPANY_MONITORING_EVIDENCE_REVISION_INVALID
- COMPANY_MONITORING_ _INVALID
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)