PromoRule:
    | string
    | {
        all: PromoRule[];
    }
    | {
        any: PromoRule[];
    }
    | {
        not: PromoRule;
    }

A promo experiment's targetRule: the feature it promotes and an optional rule. Facts: uses:<feature>, notUses:<feature>, paid, free, joinedWithin:<1-90 days> (bot added recently), serverType:<SetupServerType> and goal:<SetupGoal> (the setup wizard's answers), combined with { all: [...] }, { any: [...] }, { not: rule }. Ranges are <min>-<max>, either end open (500-, -10): joinedDays:<range> (days since the bot was added), members:<range>, limitPct:<limit>:<range> (percent of a plan limit used; 100- = at or over it), count:<limit>:<range>, and hitLimit:<limit> (that limit refused the server in the last 7 days). Limit ids come from getAdminExperimentFacts. Variant config for a promo: { title (<=80), body (<=240), cta (<=30), discordText? (<=150) } plus optional surfaces (a subset of the experiment's - this variant shows only there), ctaTarget ('feature' | 'premium' | 'checkout'; a limit promo cannot use 'feature'), a discount percentOff (5-90) + hours (1-168) - the server makes the coupon, couponId is read-only - OR trialDays (1-14). Discount and trial need goal buy. control shows nothing. discordText is the Discord reply tip when the experiment's surfaces include discord (title if unset).