Every feature, in depth

An ad server, made of listicles.
Here's everything inside it.

The deep-dive companion to the landing page. Ten sections. No marketing fluff — schemas, latency targets, allocation math, and the actual nouns the product runs on.

01 · BUILDER

Two layers of drag-and-drop. Mobile-first. Autosaves on every keystroke.

Drag ad units onto a page. Drag blocks into ad units. TipTap edits everything inline. The mobile viewport is the default — desktop is the secondary preview, not the other way around.

Capabilities

  • Page builder. Stack-based reorder of ad units. Page shell edits inline via TipTap; structural position fixed by template.
  • Ad-unit builder. Drag-drop reorder of blocks. Palette grouped Media / Text / CTA / Interactive / Structural.
  • Inline rich text. TipTap everywhere — bold, italic, links with rel + new-tab, brand color palette, h2/h3, lists.
  • Mobile-first preview. Default viewport 375px. Desktop and tablet are toggle-only. Real traffic distribution, not designer ego.
  • Autosave. 1-second debounce on every keystroke and drag. Drafts mutate indefinitely; publish snapshots a new immutable version.
  • Hover preview. Every ad-unit placement shows live RPM, CTR, version status without clicking in.
  • AI slash-commands. /rewrite punchier, /add urgency, /translate Spanish, /match competitor tone — on every text field.
block library · v1
typedescriptionfields
imageImage with promo banner overlays + star ratingsrc, alt, promoBannerText, starRating, reviewCount
headlineBold display headline (h2/h3)text, level, color, emojiPrefix
subheadlineSmaller hook line under the headlinetext, color
bodyTextMulti-paragraph rich text via TipTapcontent (TipTap JSON)
bulletListBulleted list with optional iconsitems[], style (check/dot/number)
ctaButtonSingleOne CTA with sub-param overridestext, destinationUrl, style, subParamOverrides
ctaButtonGroupMulti-button qualifier (e.g., 6-button roof age)questionText, buttons[], layout, finalCta
ctaTextLinkInline text link with arrowtext, destinationUrl, arrowStyle
urgencyCalloutURGENT: callout boxtext, bgColor, borderColor, icon
stepByStepNumbered step instructionssteps[]
stateMapUS state map (clickable in v2)highlightedStates[], linkPerState
dividerHorizontal rulestyle, spacing
spacerVertical whitespaceheight
13 v1 types · 6 deferred to v2 (video, quiz, countdown, testimonial, comparison, embed)

02 · AD UNITS

Every block is a versioned ad unit. The same unit ships across pages.

Ad units are first-class library items, not page-locked content. Place one unit on five pages, A/B-test it once, and the winning variant compounds everywhere it lives. RPM rolls up by unit, by slot, by CTA button — not just by page.

How reuse works

  • PagePlacement is a join. One adUnit row, many pagePlacement rows. Edit the unit, every placement updates.
  • Cross-page rollups. RPM aggregates by ad unit, by placement slot, by individual CTA button — not just by the page that hosts it.
  • Per-placement overrides. The join row carries optional pageSpecificOverrides for one-off tweaks without forking the unit.
  • Independent versioning. Each unit has its own version history, A/B tests, and bandit experiments. Page versioning is a separate axis.
  • Ad-unit-level A/B rotates per impression. No within-session stickiness — the swap is invisible to the visitor.
  • Co-pilot promotion. "This unit lifts RPM 18% on long-form senior pages — promote to the other 12 matching pages?" One click.

One unit, four placements, one rolled-up RPM

adUnit · qualifier-roof-age-v6

qualifier-roof-agev6 · liveroof-replacement.liveslot 2 · RPM $38.10solar-2026.liveslot 1 · RPM $44.90home-warranty.liveslot 3 · RPM $31.20roofing-deals.liveslot 2 · RPM $40.40unit-level rollupRPM $38.65 · n=12,894

03 · TRACKING

Sub-50ms click ID. Tokens pin to a version. Sub-IDs persist across sessions.

Edge function mints a clickId on every CTA tap. Tokens never silently re-resolve to a newer version — they pin at creation. Bot scoring runs before write; filtered clicks still record + redirect, just flagged.

visitor taps button · /c/8f3aJq2pK · edge resolves & redirects in <50ms p95
GET /c/8f3aJq2pK            # 16-char base62 token

resolves to (pinned at mint):
  pageVersionId    = v_e1c4…
  adUnitVersionId  = v_a04b…
  blockId          = b_qual_roof
  ctaButtonId      = cta_5_9_yrs

→ mint clickId, write row, 302 redirect:

https://offer.partner.com/click
  ?clickId=cid_8f3a4c…
  &payout={payout}         # downstream fills
  &sub1=roof_5_9        # button override wins
  &sub2=meta_camp_x     # from landing URL
  &sub3=FL              # geo_state token
  &sub4..sub10              # passthrough
  &source=tlb
isFiltered=false · bot score 0.04
latency 38ms · v ad unit v_a04b

Mechanics

  • Token format. 16-char base62. Resolves to (pageVersionId, adUnitVersionId, blockId, ctaButtonId) via lookup table.
  • Pin-on-mint. Old tokens never re-point to a newer version. Publish mints fresh tokens; cached URLs continue to work and route to the version they were minted against.
  • Sub1–sub10 + UTMs. Captured at first landing, persisted on the visitor record, attributable on a return-session click 90 days later.
  • Override hierarchy. per-button override → page-level override → visitor's captured value → empty.
  • Bot filter hook. Cloudflare Bot Mgmt + FingerprintJS Pro + internal heuristics (datacenter IP, anomalous UA, click velocity per visitor).
  • Filtered traffic. Still recorded, still redirected (don't tip off bots). Flagged isFiltered=true, excluded from bandit + default dashboards.

04 · POSTBACKS

Postbacks fire to your domain. We verify, normalize, and re-fire downstream.

Inbound postbacks land on /postback at the tenant domain — HMAC-verified, FX-normalized, deduped per integration rule. Outbound forwarding re-fires to your downstream stack with a 5-attempt retry budget over ~26 hours.

inbound · everflow · HMAC verified
POST /postback
  ?clickId=cid_8f3a4c…
  &payout=42.10
  &currency=USD
  &conversionType=sale
  &txid=ef_99201
  &eventType=conversion
  &signature=a7f9c…

X-Forwarded-For: 35.190.x.x   
# auth: hmac (constant-time)
# rate limit: 600/min
# dedupe rule: allow_if_payout_differs

→ accepted · stored · arm reward updated
outbound forwarding · param map applied
GET https://leadrouter.example.com/cv
  ?cid=cid_8f3a4c…         # clickId → cid
  &revenue=4210           # payoutAmountUsdCents
  &event=sale            # conversionType
  &sub1=roof_5_9
  &sub2=meta_camp_x

retries: 1m → 6m → 36m → 3.6h → 21.6h
on max-fail: dead-letter + alert

Configurable per integration

  • Auth method. hmac · sharedSecret · ipAllowlist · none
  • Dedupe. reject · allow_if_payout_differs · always_allow
  • Late conversions. Accepted regardless of age. No time-based rejection.
  • Currency. Daily FX. Original + USD-normalized stored side-by-side.
  • Payout updates. eventType=payout_update revises a recorded conversion without writing a duplicate.
  • Rate limit. Default 600/min per integration. Sustained 429s alert.
  • Outbound retry. 5 attempts, exponential 1m → 6m → 36m → 3.6h → 21.6h. After max, dead-letter + Slack alert + AI-suggested fix.
  • Audit retention. 90 days online (full inbound + outbound payloads), then cold storage.

AI assist

Paste downstream platform docs → co-pilot writes the param map, picks the strongest auth method that platform supports, and explains the dedupe rule that matches its retry behavior.

05 · VERSIONING

Drafts mutate. Publishes snapshot. Rollback is a new version, not a delete.

Every publish creates an immutable version row. Drafts can be edited indefinitely without polluting history. Rolling back to v4 from v7 creates v8 as a copy of v4 — the audit trail stays intact and v7 stays inspectable.

Two test surfaces

Page-level A/B

Multiple page versions live at once. Sticky per visitor via visitorId hash — same visitor sees the same variant for their session, no jarring within-session swaps.

Ad-unit-level A/B

Versions rotate per impression. The unit swap is invisible to the user, so no stickiness needed. Lets the bandit allocate fresh on every ad-unit render even if the page is sticky.

Auto-conclude

95% credible interval threshold by default. Tenants can hold for human review or set hands-off auto-promote.

Late conversion reconciliation

Postbacks accepted at any age re-weight arms. AI flags integrations where late attribution is meaningfully changing winners and suggests a delay-before-conclude window.

version history · senior-benefits.live7 versions
  • v7Apr 28, 14:02

    New hero · urgency callout · headline shorter

    by Linda C. · RPM $42.10 · CTR 4.8%

    Live
  • v6Apr 21, 09:14

    Swapped CTA color · added trust strip

    by Linda C. · RPM $36.40 · CTR 4.2%

    Archived
  • v5Apr 14, 17:30

    AI-generated challenger headline (won)

    by AI co-pilot · RPM $33.10 · CTR 3.9%

    Archived
  • v4Apr 09, 11:48

    Rollback target for v8 if v7 underperforms

    by R. Patel · RPM $31.20 · CTR 3.8%

    Rollback target
  • v3Apr 02, 08:01

    Initial publish

    by Linda C. · RPM $28.90 · CTR 3.5%

    Archived
diff between any two versions · one-click rollback creates v8 (copy of v4)

06 · BANDIT

Thompson Sampling on RPM. CTR-warmed cold-start. 5% allocation floor.

The bandit optimizes revenue per mille — but RPM is a slow signal. Until each arm crosses 50 conversions, it samples Beta-Bernoulli on click-through. After threshold, it switches to normal-inverse-gamma on RPM. No arm ever drops below 5% allocation, and the AI always has three fresh challengers queued.

3 arms · last 14 days · sticky page assignmentRPM, USD
Arm B · 86%Arm A · 11%Arm C · 3%
Arm B
86%

$42.10 · n=2,184

95% CI ±$2.40

Arm A
11%

$28.40 · n=2,011

95% CI ±$3.10

Arm C
3%

$19.80 · n=540

95% CI ±$5.80

P(B is best) 96.2%cold-start ✓ all arms ≥ 50 convallocation cache 60s

Math, in plain English

  • Cold-start. < 50 conversions per arm → Beta(α, β) on click events. reward = 1 if click else 0.
  • Warm phase. Continuous Thompson Sampling on RPM with normal-inverse-gamma priors. reward = revenueUsdCents / impressions.
  • Sticky page assignment. Page-level bandit hashes visitorId to keep variant assignment stable across the visitor's session.
  • Per-impression at unit level. Ad-unit bandits draw a fresh sample on every render. No stickiness needed — invisible swaps.
  • Allocation floor. 5% minimum per arm. Prevents starvation that would block learning.
  • Update cadence. Allocation re-computed every 1-minute rollup window.
  • Auto-challengers. AI generates three new variants — different headline, different CTA, different hero — each round, so the bandit always has new contestants to spar against the leader.

07 · ANALYTICS

IAB-viewable impressions. Pre-aggregated rollups. Drill on every dimension.

Impressions fire only when a unit hits 50% pixels in view for one continuous second. Events go to Postgres, then Inngest cron rolls up to minute / hour / day tables. At ~500K page views/day, the same dashboard layer points at Tinybird without rewriting.

Impressions

IAB-viewable: 50% pixels in view ≥ 1 continuous second

Clicks

CTA tap, server-side recorded, bot-filtered traffic flagged

CTR

clicks / impressions, filtered traffic excluded

Conversions

Inbound postback received, after dedupe rule applied

Conversion rate

conversions / clicks

Revenue

USD-normalized via daily FX, original amount also stored

EPC

revenue / clicks — earnings per click

RPM / ECPM

revenue per 1,000 impressions — primary bandit metric

RPS

revenue per session — visitor-level monetization

Visitors

Unique visitorIds in window (90-day cookie)

Sessions

visitorId observed after ≥ 30 min inactivity

Pages / session

Page views ÷ sessions

Drill-down dimensions

utmSourceutmMediumutmCampaignutmContentutmTermsub1sub2sub3sub4sub5sub6sub7sub8sub9sub10countrystatecitydeviceClassbrowserospageVersionIdadUnitVersionIdplacementSlotctaButtonIdbanditArmId

Every dimension can be sliced on every metric. Rollup tables enforce UNIQUE … NULLS NOT DISTINCT so missing UTMs aggregate cleanly.

Dashboards

  • Overview
  • Pages
  • Ad Units
  • Experiments
  • Traffic Sources
  • Geo
  • Device
  • Conversions
  • Integrations

Phase 2

At ~500K page views/day, ingestion migrates to Tinybird. Postgres remains source of truth during the cutover; the dashboard query layer is unchanged.

08 · CO-PILOT

One co-pilot. It writes through your role, your domain scope, your audit log.

Build, Diagnose, Optimize, Compose, Explain, Operate, Migrate, Onboard. Same tenant context as a human teammate. Every action is a draft until you approve it — there is no AI auto-publish.

builder · senior-benefits
BUILD
YOU
Build a senior dental discounts page for Florida — 5 ad units, urgency framing, quiz-style qualifier on unit 3.
AI
Drafted: page senior-dental-fl, template long-form-comparison.
· 5 ad units placed, slot order by historical RPM in this vertical.
· Unit 3: ctaButtonGroup 6-button age qualifier (65–69, 70–74, 75–79, 80–84, 85+, Other) with sub1 overrides pre-filled.
· Unit 5: urgencyCallout tied to FL open enrollment window.
→ open in builder · publish requires your approval
senior-benefits.live · last 7d
DIAGNOSE
YOU
RPM dropped 22% on senior-benefits.live since Tuesday — why?
AI
Decomposed across 6 dimensions. Two signals matter:
· sub3 mix shifted 41%→62% toward taboola_desktop_us (lower-RPM cohort, −$8.20 RPM).
· Ad unit 4 RPM collapsed −38% after v12 publish Tue 14:02 — new hero pushed CTA below the fold on iOS Safari.
→ rollback v12 → v11 + pause taboola_desktop_us · est recovery +$4,800/wk
experiments · roof-replacement
OPTIMIZE
YOU
What should I test next on this page?
AI
Hypothesis: trust signals above the fold lift CTR for cold Meta traffic on this vertical. Predicted lift +6–11% CTR, 90% CI.
· Add BBB + secured-checkout strip above the qualifier.
· Keep the urgencyCallout below the qualifier for the test arm.
→ create experiment · 3 challenger variants drafted
onboarding · paste-a-url
MIGRATE
YOU
Rebuild this competitor as a tenant-owned page: https://other-site.com/senior-discounts-fl
AI
Parsed page · 7 ad units detected · 1 quiz-style qualifier · 2 urgency callouts.
· Mapped to long-form-comparison template.
· Brand colors swapped to your kit · FTC disclosure auto-added · author persona set to L. Chen.
· All CTA destinations cleared — point them at your offers before publish.
→ open draft · ready in builder

09 · TENANTS

Wildcard subdomain on signup. Custom domain with a super-admin gate.

Every tenant gets {slug}.thelisticlebuilder.com instantly. Custom domain attaches via Vercel after a super-admin abuse-filter approval — paste a domain and the AI pre-screens reputation before it hits the queue. BetterAuth handles magic link + Google OAuth. Audit log captures every state change with before/after diff.

What ships out of the box

  • BetterAuth. Magic link + Google OAuth in v1. Passkey + SAML deferred to v2 (no v1 demand justifies the friction).
  • Membership state machine. invited → accepted → suspended → revoked, transitions audit-logged.
  • Domain scope. Each membership scopes to all or a list of authorizedDomainIds. Editor on veteran-discounts only? Done.
  • Wildcard subdomain. {slug}.thelisticlebuilder.com live within seconds of signup. AI suggests slug variants tuned to vertical.
  • Custom domain pipeline. Vercel domains API + Let's Encrypt, gated by super-admin queue. AI pre-screens reputation (prior content, blocklists) before queue insert.
  • Audit log. Every state change captured with before/after diff. 1-year online retention, then archived.
permission matrix · v1
CapabilityOwnerAdminEditorViewer
Manage tenant settings + billing
Invite + revoke teammates
Attach custom domain
Create / edit pages + ad units
Publish a version
Configure integrations + secrets
Conclude experiments
View dashboards + audit log
Domain scope (restrict per user)

10 · COMPLIANCE

Disclosures default-on. Bots filtered before write. Hybrid SSR keeps CWV honest.

FTC disclosure is a required block in every template, with a vertical-aware library tuned per offer. Cookie consent is geo-detected at the edge. Bot filtering layers Cloudflare → FingerprintJS Pro → internal heuristics. Page shell ISR-caches 60s; ad units stream in via Suspense — LCP target <2.0s on mobile 4G.

Default-on

FTC disclosures

  • Required block in every template + footer.
  • Vertical library: general, health (FDA-aware), financial (CFPB-aware), legal (mass-tort aware).
  • AI flags risky claims ("guaranteed approval", "FDA-approved") and proposes safer rewrites.
  • AI auto-generates the privacy policy from actual data flows + integrations.

Geo-detected at edge

GDPR + CCPA

  • EU + California visitors see consent banner before any non-essential cookie.
  • visitorId is essential (fraud + analytics) — set without consent.
  • Marketing cookies (e.g., FingerprintJS for non-fraud) gated behind explicit opt-in.
  • Consent cookie respected across the domain. Re-openable from footer.

Three layers

Bot filtering

  • Cloudflare Bot Management header on inbound traffic.
  • FingerprintJS Pro fingerprint on the page.
  • Internal heuristics: datacenter IP, anomalous UA, click velocity per visitor.
  • Filtered traffic still records + redirects (don't tip off bots). Excluded from bandit + default dashboards.

Hybrid SSR rendering

  • Page shell. Server components, ISR-cached 60s, revalidated on publish via revalidatePath. Sub-100ms TTFB at the edge.
  • Ad units stream. Each unit renders inside <Suspense>, with a 5–30s bandit decision cache balancing freshness against function cost.
  • Builder code never ships. TipTap and the editor are admin-only. Published pages target <100KB gzipped JS.
  • CWV targets. LCP < 2.0s mobile 4G · CLS < 0.05 · INP < 200ms. AI weekly-scans every published page and flags the three biggest offenders per tenant.

Asset pipeline

  • R2 + Cloudflare Images. Direct presigned upload, AVIF / WebP / JPEG responsive delivery, EXIF stripped on ingest.
  • Alt text required. AI auto-generates alt text on upload; manual override always available.
  • Above-the-fold. Eager-loaded with explicit width/height — no CLS from late-arriving hero images.
  • Fraud reports. Tenants flag suspect traffic; AI clusters patterns and lets super-admins one-click bulk-flag with a written reason.

Ready to ship your first listicle?

Register a domain in the morning. Run paid traffic to a polished, self-optimizing page by dinner. Free to start.