Skip to main content

Advertising Config

Pricing for sponsors and paid placements — Stripe wiring, scarcity caps, bundle discounts and the env-var checklist.

Why this matters

Ad placement pricing is one of those decisions that looks trivial in config but compounds into real revenue. A directory with 5,000 monthly pageviews and $69 banner pricing earns ~$70/month per active banner; one with the same traffic and $39 pricing earns $40 — but the cheaper option may sell faster and stay sold longer. The right price for your directory depends on niche, audience and how many slots you cap.

This page covers the file's mechanics, the Stripe wiring, and the strategic levers (scarcity caps, bundle discounts) that make the system work. Read Common pitfalls before going to production — most issues we see are misconfigured Stripe Prices that quietly break renewals.

File: config/advertising.config.ts

Controls pricing and limits for the three paid placements and for sponsor subscriptions. Each placement needs a matching Stripe Price ID.

What this controls

Sponsors

Monthly subscription for logos in the sidebar / partners section.

sponsors: {
  maxSponsors: 8,
  priceId: process.env.STRIPE_PRICE_ID_PARTNER_SUBSCRIPTION || null,
},
FieldMeaning
maxSponsorsHow many sponsor slots are active at any time. Extra buyers go on a waitlist.
priceIdRecurring Stripe price — created in Stripe Dashboard.

Promotions (three placement types)

promotions: {
  ctaMaxLength: 20,
  ctaDefault: 'Visit {name}',
  allThreeDiscountPercent: 0.3,
  allThreeDiscountCouponId: process.env.STRIPE_COUPON_ID_PROMO_ALL_THREE || null,
  minPricePerMonth: 19,
  placements: {
    banner: {
      id: 'banner',
      name: 'Top Banner Ad',
      description: 'Horizontal banner below the header',
      pricePerMonth: 69,
      priceId: process.env.STRIPE_PRICE_ID_PROMO_BANNER || null,
      maxActive: 1,
    },
    catalog: {
      id: 'catalog',
      name: 'Catalog Ad Card',
      description: 'Promoted card in the project grid',
      pricePerMonth: 39,
      priceId: process.env.STRIPE_PRICE_ID_PROMO_CATALOG || null,
      maxActive: 2,
    },
    detailPage: {
      id: 'detail_page',
      name: 'Project Page Ad Card',
      description: 'Sidebar card on project detail pages',
      pricePerMonth: 19,
      priceId: process.env.STRIPE_PRICE_ID_PROMO_DETAIL || null,
      maxActive: 2,
    },
  },
},

Fields explained

FieldMeaning
ctaMaxLengthMax characters in the custom button text on a promo card.
ctaDefaultFallback CTA if user didn't set one. {name} is replaced with the project name.
allThreeDiscountPercentDiscount when buyer picks all three placements at checkout.
allThreeDiscountCouponIdStripe coupon ID used to apply that discount.
minPricePerMonthCheapest placement price — used as "starts at" on the /promote page.
maxActiveHow many of that placement type can run simultaneously.

Stripe setup

For each placement, create a recurring (monthly) Price in Stripe and paste the ID into .env.local:

STRIPE_PRICE_ID_PROMO_BANNER=price_...
STRIPE_PRICE_ID_PROMO_CATALOG=price_...
STRIPE_PRICE_ID_PROMO_DETAIL=price_...
STRIPE_PRICE_ID_PARTNER_SUBSCRIPTION=price_...

For the "buy all three = 30% off" coupon:

  1. Stripe Dashboard → CouponsCreate
  2. Percentage off: 30, Duration: forever
  3. Copy the coupon ID → STRIPE_COUPON_ID_PROMO_ALL_THREE

Turning promotions off entirely

Disable the promotions flag in features.config.ts. The /promote page returns 404, the API routes return 404, the admin section disappears.

A real-world walkthrough: pricing for a brand-new niche directory

A directory in the "Tools for Notion users" niche, expecting 3,000 monthly pageviews in month two. The pricing config that makes sense:

banner: { pricePerMonth: 49, maxActive: 1 },        // 1 slot, scarce
catalog: { pricePerMonth: 29, maxActive: 2 },       // 2 slots, moderate
detailPage: { pricePerMonth: 14, maxActive: 4 },    // 4 slots, abundant
sponsors: { maxSponsors: 5 },                       // Tiny — keep premium
allThreeDiscountPercent: 0.25,                      // Bundle = $69/month

Why these numbers: at 3,000 pageviews, the banner sees every page (3,000 impressions/month), worth ~$15–30 in CPM terms but valuable as positioning. The discount-bundle ($69/mo for all three) gives buyers an obvious "premium tier" upgrade path. Sponsor count stays at 5 — fewer slots = higher perceived value, easier to sell at $200/mo.

Two months later, when traffic doubles to 6,000, raise prices proportionally (don't double — increase 30–50%). The discount bundle should always be 20–30% cheaper than buying each separately, not free; users see through "all-three free" deals.

Common pitfalls

  • Using a one-time Price ID instead of a recurring one. Promotions and sponsors require recurring monthly Stripe Prices. A one-time Price will charge once and never renew, so the placement runs forever for free. Always create the Price with Type: Recurring, Billing period: Monthly.
  • Forgetting to test the coupon flow. The 30%-off bundle coupon works only if STRIPE_COUPON_ID_PROMO_ALL_THREE is set to a valid coupon ID. Test with a real Stripe Checkout session in test mode before launching — coupons fail silently if mistyped.
  • Setting maxActive: 99 to "be flexible". Unlimited slots dilute scarcity and tank the price you can charge. Cap meaningfully — maxActive: 1–4 per type for most niches. Above that, the placement loses premium feel.
  • Hardcoding price numbers in admin UI. All pricing comes from this config. If you see a price hardcoded in app/promote/page.tsx, fix it — single source of truth saves you when you A/B test pricing.
  • Mixing test-mode and live-mode Price IDs. A live deploy with a test-mode price_... ID will accept the checkout session but Stripe rejects the payment. Match the env var to your STRIPE_SECRET_KEY mode.

FAQ

Can I offer yearly promotions at a discount?

Yes — create a second Stripe Price (Recurring, Yearly) for each placement and surface both options at /promote. Most operators see ~20% of buyers choose yearly when offered, which improves cash flow and reduces churn at minimal cost.

How do I run a limited-time discount?

Stripe Coupons support time-bounded discounts. Create a coupon with Duration: Once and surface it as a checkout option for, say, the first 90 days post-launch. After expiry, hide the option from the UI but leave existing subscriptions grandfathered.

Is minPricePerMonth enforced anywhere?

Only on the marketing page (the "starts at $X" copy). It's not a hard validation — you could set pricePerMonth: 5 on a placement and minPricePerMonth: 19 and the page would lie. Keep them aligned.

With AI

AI Prompt· Design promotion & sponsor pricing

Update config/advertising.config.ts for my directory about {your niche}.

My monetization plan:

  • Banner (top of every page) — {price}/month, {number} slot
  • Catalog ad card (mixed into project grid) — {price}/month, {number} slots
  • Project-page ad card (sidebar) — {price}/month, {number} slots
  • Bundle discount if buying all three: {percentage}% off
  • Sponsors (logos in sidebar / partners section) — {price}/month recurring, {max} active

Default CTA fallback when buyer doesn't set one: "Visit {name}" Max chars for custom CTA: 20

Set pricePerMonth, maxActive, and helper fields accordingly. Reference env vars (STRIPE_PRICE_ID_PROMO_*, STRIPE_PRICE_ID_PARTNER_SUBSCRIPTION, STRIPE_COUPON_ID_PROMO_ALL_THREE) on the priceId / couponId fields — don't hardcode. List the env vars I need to add.

See also: Promotions & Sponsors