+
+ >
+ );
+}
diff --git a/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx b/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx
index 354d6af9..9179c1a5 100644
--- a/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx
+++ b/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx
@@ -3,6 +3,7 @@ import { DirectoryFilterController } from "@/app/ui/directories/directory-filter
import { DirectoryCheckboxGroup, DirectoryRadioGroup, DirectoryTagSelectGroup } from "@/app/ui/directories/directory-filters";
import { SortDropdown } from "@/app/ui/directories/directory-sort-dropdown";
import { Example } from "../gallery";
+import { ToolbarDemo } from "./ad-network-demos";
import styles from "../components-ui.module.css";
export function FiltersAndSortingExamples() {
@@ -35,6 +36,14 @@ export function FiltersAndSortingExamples() {
+
+
+
>
);
}
diff --git a/app/(site)/(components-ui)/components-ui/examples/navigation.tsx b/app/(site)/(components-ui)/components-ui/examples/navigation.tsx
index eba40776..8c46991c 100644
--- a/app/(site)/(components-ui)/components-ui/examples/navigation.tsx
+++ b/app/(site)/(components-ui)/components-ui/examples/navigation.tsx
@@ -2,6 +2,7 @@ import { Globe, HardDrive, Keyboard } from "lucide-react";
import { Pagination } from "@/app/ui/pagination";
import { Breadcrumbs } from "@/app/ui/site/breadcrumbs";
import { SidebarList } from "@/app/ui/site/sidebar-list";
+import { UiTabs } from "@/app/ui/tabs";
import { Example } from "../gallery";
import styles from "../components-ui.module.css";
@@ -56,6 +57,22 @@ export function NavigationExamples() {
+ },
+ { id: "pay", title: "Pay", content: },
+]} />` }}
+ >
+ Who can apply, the traffic rule, and how to apply.
},
+ { id: "pay", title: "Pay", content:
Your share, when you get paid, and the payment minimum.
},
+ { id: "rules", title: "Rules", content:
Ad placement, other ad partners, and the contract.
},
+ ]} />
+
>
);
}
diff --git a/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx b/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx
index c6499e71..a379d498 100644
--- a/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx
+++ b/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx
@@ -1,4 +1,5 @@
import Link from "next/link";
+import { AdSection, SectionNote } from "@/app/ui/ad-networks/section";
import { HardDrive } from "lucide-react";
import { siteHeroStack127Art } from "@/app/(site)/art/site--hero-stack127";
import { BrandWordmark } from "@/app/ui/site/brand-wordmark";
@@ -111,6 +112,21 @@ export function PageLayoutExamples() {
))}
+
+
+
+ Dates before Jul 2026 come from archived copies of its ads.txt in the Wayback Machine.
+
+ controls sit here} description="Every network that runs on food sites, with what it takes to join." id="section-demo-2" title="Which ad networks do food sites use?">
+ The next section starts after a hairline.
+
+
You apply with your site and give Mediavine read access to your Google Analytics. They check your traffic and content against the rules here.
+
What does the pay day mean?
Mediavine pays each month's earnings a set number of days after the month ends.
+ >
+ )}
+ title="What does Mediavine require and pay?"
+ titleId="details-demo"
+ >
+
+ },
+ ]} /> },
+ { id: "pay", title: "Pay", content: },
+ ]} />
+
+
+
+
+ },
+ { label: "niche", value: "Food" },
+ { label: "platform", value: "WordPress" },
+ { label: "domain registered", value: <>2014 · 12 yrs> },
+ { label: "publisher", value: "Tiny Kitchen Lab LLC" },
+ ]} />
+
+
+
+
+ companies with the most lines
+
+
+
+
+
+
+
+
>
);
}
diff --git a/app/(site)/(components-ui)/components-ui/examples/tables.tsx b/app/(site)/(components-ui)/components-ui/examples/tables.tsx
index 51b6fecc..70251017 100644
--- a/app/(site)/(components-ui)/components-ui/examples/tables.tsx
+++ b/app/(site)/(components-ui)/components-ui/examples/tables.tsx
@@ -1,6 +1,10 @@
import { ContentTable } from "@/app/ui/content-elements";
import { UiDataTable } from "@/app/ui/display";
import { Example } from "../gallery";
+import { NetworkLadder } from "@/app/ui/ad-networks/network-ladder";
+import { NichesTable } from "@/app/ui/ad-networks/niches-table";
+import { SiteNetworksTable } from "@/app/ui/ad-networks/site-view";
+import { SitesTableDemo } from "./ad-network-demos";
export function TablesExamples() {
return (
@@ -43,6 +47,67 @@ export function TablesExamples() {
+
+
+
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/app/(site)/(components-ui)/components-ui/sections.ts b/app/(site)/(components-ui)/components-ui/sections.ts
index 297c27f5..e2466335 100644
--- a/app/(site)/(components-ui)/components-ui/sections.ts
+++ b/app/(site)/(components-ui)/components-ui/sections.ts
@@ -1,4 +1,5 @@
import {
+ ChartBarBig,
ChartColumn,
FileText,
Gauge,
@@ -65,21 +66,21 @@ export const gallerySections: GallerySection[] = [
label: "Filters and sorting",
description: "Search, sort, and filter groups for any long list of records.",
icon: ListFilter,
- components: ["SortDropdown", "DirectoryCheckboxGroup", "DirectoryRadioGroup", "DirectoryTagSelectGroup", "DirectoryFilterController"],
+ components: ["SortDropdown", "DirectoryCheckboxGroup", "DirectoryRadioGroup", "DirectoryTagSelectGroup", "DirectoryFilterController", "TableToolbar", "ToolbarSearch", "ToolbarSelect", "ToolbarSwitch"],
},
{
slug: "results",
label: "Results",
description: "The main answer, supporting numbers, status words, progress, steps, timelines, and generated output.",
icon: Gauge,
- components: ["UiPrimaryResult", "UiResultLead", "UiMetricGroup", "UiKeyValueList", "UiStatusWord", "UiProgressBar", "UiAllocationBar", "UiRangeMarker", "UiUnitGrid", "UiSteps", "UiTimeline", "UiCodeOutput", "UiRawOutput"],
+ components: ["UiPrimaryResult", "UiResultLead", "UiMetricGroup", "UiKeyValueList", "UiStatusWord", "UiProgressBar", "UiAllocationBar", "UiRangeMarker", "UiUnitGrid", "UiSteps", "UiTimeline", "UiCodeOutput", "UiRawOutput", "Report", "ReportGroup", "ReportRow", "ReportBlock", "ReportNumbers", "ReportFigure", "ReportBars", "KeyNumbers", "FactsList", "NotPublic", "DetailsLayout", "SourcesLine", "SiteFacts", "AdsTxtNumbers", "AdsTxtPreview", "SimilarSites", "TrafficMeter"],
},
{
slug: "tables",
label: "Tables",
description: "Data tables with row headers, content tables written in MDX, and a scroll frame for wide tables.",
icon: Table2,
- components: ["UiDataTable", "ContentTable", "TableFrame"],
+ components: ["UiDataTable", "ContentTable", "TableFrame", "SitesTable", "NichesTable", "NetworkLadder", "SiteNetworksTable"],
},
{
slug: "charts",
@@ -88,12 +89,19 @@ export const gallerySections: GallerySection[] = [
icon: ChartColumn,
components: ["UiChart", "UiStaticChart", "UiLiveChart"],
},
+ {
+ slug: "data-visuals",
+ label: "Data visuals",
+ description: "Bars, stacks, rings, columns, lines, flows, strips, treemaps, grids, and timelines drawn on the server in the site style, with small scripts only for hover, keys, and width. The ad networks pages use these.",
+ icon: ChartBarBig,
+ components: ["RankedBars", "StackBar", "MoveBars", "VizLegend", "Donut", "TypeScale", "PublisherBlocks", "ColumnChart", "AreaLine", "JoinedLeftBars", "SwitchFlow", "AgeStrip", "SizeStacks", "NewestList", "Treemap", "Heatmap", "MultiLine", "DivergingBars", "ScatterPlot", "DotPlot", "StackedColumns", "HistoryGantt", "EventsList"],
+ },
{
slug: "cards",
label: "Cards",
description: "Linked page cards and record cards. Any page can list them.",
icon: SquareStack,
- components: ["PageCard", "PageCardCounts", "ToolCard", "CatalogCard", "DirectoryCard", "AppFactCardContent", "DirectoryAppCard", "HardwareFactCardContent"],
+ components: ["PageCard", "PageCardCounts", "ToolCard", "CatalogCard", "DirectoryCard", "AppFactCardContent", "DirectoryAppCard", "HardwareFactCardContent", "NetworkLogo", "NetworkCard", "NetworkGrid", "SortNote"],
},
{
slug: "content",
@@ -128,14 +136,14 @@ export const gallerySections: GallerySection[] = [
label: "Navigation",
description: "Breadcrumbs above page titles, page links for long lists, and sidebar link lists.",
icon: Signpost,
- components: ["Breadcrumbs", "Pagination", "SidebarList"],
+ components: ["Breadcrumbs", "Pagination", "SidebarList", "UiTabs"],
},
{
slug: "page-layout",
label: "Page layout",
description: "Page headers, sidebar positions, the section bar, the entry hero, page metadata, brand pieces, and the tool workbench.",
icon: LayoutTemplate,
- components: ["PageHeader", "PageTitleIcon", "CategoryIcon", "EntryHero", "PageMeta", "BrandWordmark", "ThemeToggle", "ToolWorkbench", "ToolFieldRow", "ToolResult", "SectionBar"],
+ components: ["PageHeader", "PageTitleIcon", "CategoryIcon", "EntryHero", "PageMeta", "BrandWordmark", "ThemeToggle", "ToolWorkbench", "ToolFieldRow", "ToolResult", "SectionBar", "AdSection", "SectionHead", "SectionNote"],
},
];
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx
index 7531ce64..49221383 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Who is Adapex a fit for?
+
+Adapex suits a publisher who already sells ads through Google and wants a team to manage the setup. You keep your own Google Ad Manager account, so you aren't starting from zero.
+
+## Can you try Adapex without a long contract?
+
+Yes. Adapex says no long-term contract is needed. It offers better terms on longer contracts, so ask what you'd give up by staying short.
+
+## What does onboarding through MCM mean?
+
+MCM is Google's way of letting a partner manage ads for another publisher. Your Ad Manager account stays yours, and Adapex works with it under that link.
+
+## How can you check if a site uses Adapex?
+
+Look for Adapex in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Adapex.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx
index 1bb3f8d1..45c9ae99 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx
@@ -1 +1 @@
-Publisher ad monetization service. A site can use more than one ad network.
+Adapex is a managed ad service that runs publishers' ads through Google Ad Manager. It suits sites already on AdSense, Ad Exchange, or Prebid that want a team to manage them.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/joining.mdx
new file mode 100644
index 00000000..d8cd9d5b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Adapex?
+
+Adapex doesn't publish a traffic minimum, site rules, or the regions it accepts. You apply through the contact form on its site.
+
+If you already run AdSense, Ad Exchange, or Prebid, Adapex can bring that setup in. Onboarding goes through Google Ad Manager's Multiple Customer Management (MCM).
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/pay.mdx
new file mode 100644
index 00000000..479ef08a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/pay.mdx
@@ -0,0 +1,3 @@
+## How does Adapex pay?
+
+Adapex pays on Net 60 terms, so each month's earnings arrive 60 days after that month ends. Its share, payment minimum, payment methods, and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/rules.mdx
new file mode 100644
index 00000000..6803a0a6
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Adapex set?
+
+Adapex doesn't require a long-term contract. A longer contract can get you a better share or extra features.
+
+You can block ad categories, advertisers, and domains, and set price floors. Whether Adapex must be your only ad partner, and how much notice you give to leave, are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/service.mdx
new file mode 100644
index 00000000..360d85a6
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/service.mdx
@@ -0,0 +1,3 @@
+## What does Adapex do for you?
+
+You get a dashboard that reports revenue and traffic close to real time, split by country and device. You keep your own Google Ad Manager account.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx
index 7531ce64..85aa78c8 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Who is Aditude for?
+
+Aditude works with web and app publishers on their ad technology. It doesn't say who qualifies, so the only way to know is to ask.
+
+## What should you ask Aditude before you sign?
+
+Ask for the share you'd get, when you'd be paid, and whether Aditude must be your only ad partner. Ask how much notice you'd need to leave. None of this is on its public pages, so get it in writing.
+
+## How can you check if a site uses Aditude?
+
+Look for Aditude in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Aditude.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx
index 087fd774..7fb90bd2 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx
@@ -1 +1 @@
-Publisher ad technology platform. A site can use more than one ad network.
+Aditude is an ad technology company that builds and runs ad setups for web and app publishers. Its joining rules and terms are not public, so you'll need to talk to its team.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/joining.mdx
new file mode 100644
index 00000000..01f46e54
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Aditude?
+
+Not public. Aditude doesn't publish a traffic minimum, site rules, accepted regions, or how to apply. Contact its team to ask whether your site fits.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/pay.mdx
new file mode 100644
index 00000000..2b2af237
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/pay.mdx
@@ -0,0 +1,3 @@
+## How does Aditude pay?
+
+Not public. Aditude doesn't publish its share, when it pays, the payment minimum, methods, or currency.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/rules.mdx
new file mode 100644
index 00000000..52ebf01a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Aditude set?
+
+Not public. The terms on Aditude's site cover use of its website, not a publisher agreement. Ask about exclusivity, ad placement, content rules, and notice to leave before you sign.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/service.mdx
new file mode 100644
index 00000000..11633a6e
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/service.mdx
@@ -0,0 +1,3 @@
+## What does Aditude do for you?
+
+Aditude publishes guides on ad technology for publishers. Its dashboard, reporting, and support terms are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx
index 7531ce64..b259d509 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is Adnimation a fit for a small site?
+
+Probably not the managed service, since it asks for substantial traffic. Adnimation's MAP product has no pageview minimum, but it's a separate program with its own terms.
+
+## Can you leave Adnimation?
+
+Yes. There's no lock-in contract, but you need to give notice. Ask how much before you sign, since it isn't public.
+
+## What ads will Adnimation run on your site?
+
+Display, video, and native or recommendation ads, with header bidding behind them. You can ask to change where they sit on your pages.
+
+## How can you check if a site uses Adnimation?
+
+Look for Adnimation in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Adnimation.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx
index 2e74bcb9..2120ab7b 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx
@@ -1 +1 @@
-Publisher ad management service. A site can use more than one ad network.
+Adnimation is a managed ad service for publishers with steady traffic. It runs display, video, and native ads for you.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/joining.mdx
new file mode 100644
index 00000000..50cdcf62
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/joining.mdx
@@ -0,0 +1,7 @@
+## Who can join Adnimation?
+
+Adnimation asks for substantial traffic but doesn't publish a number. It reviews each site, and you apply by contacting its team. It doesn't say which regions it accepts.
+
+Sites with gambling, porn, or drug content are restricted.
+
+Adnimation's separate MAP product says it has no pageview minimum. That's a different program from the managed service on this page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/pay.mdx
new file mode 100644
index 00000000..80342fd2
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/pay.mdx
@@ -0,0 +1,5 @@
+## How does Adnimation pay?
+
+Adnimation pays monthly by direct bank payment, on Net 35 terms, and it may accept PayPal. It works on a revenue share, but the rate is not public. The payment minimum and currency are not public either.
+
+The MAP program has its own payment terms, which don't apply to the managed service.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/rules.mdx
new file mode 100644
index 00000000..758723d0
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Adnimation set?
+
+There's no lock-in contract, but you have to give notice to leave. How much notice is not public.
+
+You can ask for changes to where ads go on your pages. Adnimation doesn't publish a rule that it must be your only ad partner.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/service.mdx
new file mode 100644
index 00000000..72c475c0
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/service.mdx
@@ -0,0 +1,5 @@
+## What does Adnimation do for you?
+
+Your dashboard shows gross and net earnings. Adnimation runs display, header bidding, video, CTV and OTT, app, and native or recommendation ads, with lazy loading and ad refresh.
+
+Support is by email, phone, WhatsApp, Telegram, and Skype. Adnimation says it replies in under two hours on average.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx
index 7531ce64..f94fbfdc 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Who is AdPushup for?
+
+Publishers who want help managing ad placements and price floors. AdPushup doesn't publish who qualifies, so submit your site and see what its review says.
+
+## Are AdPushup's public terms current?
+
+Maybe not. The public terms are several years old and may not match today's agreements. Read the agreement you're offered, and go by that.
+
+## How can you check if a site uses AdPushup?
+
+Look for AdPushup in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to AdPushup.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx
index 48375e7e..944f3f90 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx
@@ -1 +1 @@
-Publisher ad revenue optimization platform. A site can use more than one ad network.
+AdPushup is an ad revenue platform that helps publishers manage ad placements and price floors. You submit your site for review to get started.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/joining.mdx
new file mode 100644
index 00000000..e7c6701d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join AdPushup?
+
+AdPushup doesn't publish a traffic minimum, accepted regions, or content rules. You submit your site on its onboarding page, and its team reviews it.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/pay.mdx
new file mode 100644
index 00000000..058d2be4
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/pay.mdx
@@ -0,0 +1,5 @@
+## How does AdPushup pay?
+
+The public numbers come from AdPushup's older terms, revised in 2018. They set a default 20% service fee, so you keep 80%, with a $50 minimum and payment on Net 45. Your current agreement may differ, and that agreement is what counts.
+
+Payment methods and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/rules.mdx
new file mode 100644
index 00000000..3247d813
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does AdPushup set?
+
+The older terms ban invalid traffic and other policy breaks. Current rules on exclusivity, ad placement, content, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/service.mdx
new file mode 100644
index 00000000..a37d285e
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/service.mdx
@@ -0,0 +1,3 @@
+## What does AdPushup do for you?
+
+You get a custom dashboard with real-time reports. You can control ad placements and price floors, and its team supports you.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx
index 7531ce64..f6302c9c 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Can you run APS with another ad network?
+
+Yes, for UAM. It's built to run next to your other header bidding partners, so you don't have to drop them.
+
+## How do you get access to APS?
+
+Contact APS and ask. UAM is invitation only, and there's no public form. It suits sites that already use Google Ad Manager.
+
+## What does the UAM fee mean for you?
+
+APS takes its fee out of each bid before the auction runs. So the bids that compete for your ad space are already net of it.
+
+## How can you check if a site uses Amazon Publisher Services?
+
+Look for Amazon Publisher Services in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Amazon Publisher Services.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx
index 1a6ea971..62eeefaf 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx
@@ -1 +1 @@
-Amazon advertising and header bidding services for web and app publishers. A site can use more than one ad network.
+Amazon Publisher Services (APS) sells publishers' ad space to Amazon and other buyers. Its Unified Ad Marketplace is invitation only and suits sites that already run Google Ad Manager.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/joining.mdx
new file mode 100644
index 00000000..11f639a0
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Amazon Publisher Services?
+
+The Unified Ad Marketplace (UAM) is invitation only. It's for web publishers who represent their own sites directly and use Google Ad Manager. APS says it works with publishers worldwide.
+
+There's no public traffic minimum or application form. Contact APS to ask for access. These rules are for UAM, not every APS product.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/pay.mdx
new file mode 100644
index 00000000..7b668a77
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/pay.mdx
@@ -0,0 +1,5 @@
+## How does Amazon Publisher Services pay?
+
+UAM takes a 10% fee from bid prices before the auction runs. Your UAM earnings come in one monthly payment, on Net 60 terms.
+
+Payment methods and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/rules.mdx
new file mode 100644
index 00000000..4ba35433
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Amazon Publisher Services set?
+
+UAM can run alongside your other header bidding, and its code can sit outside your wrapper. Exclusivity, content rules, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/service.mdx
new file mode 100644
index 00000000..5901d3de
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/service.mdx
@@ -0,0 +1,3 @@
+## What does Amazon Publisher Services do for you?
+
+Not public. The UAM documents don't describe a dashboard, reporting, or support.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx
index 7531ce64..55fe6cec 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Who is AnyManager for?
+
+Web and app publishers who want header bidding and pricing handled in one platform. AnyMind doesn't publish who qualifies, so contact its team.
+
+## How does AnyMind set your share?
+
+You agree on a fixed share before you start. Ask for it in writing, since AnyMind doesn't publish it.
+
+## How can you check if a site uses AnyManager?
+
+Look for AnyManager in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to AnyManager.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx
index 609dd2c5..ffef3327 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx
@@ -1 +1 @@
-AnyMind platform for web and app publisher monetization. A site can use more than one ad network.
+AnyManager is AnyMind's monetization platform for web and app publishers. Its joining rules and rates are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/joining.mdx
new file mode 100644
index 00000000..7b6b4bd5
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join AnyManager?
+
+Not public. AnyMind doesn't publish a traffic minimum, site rules, accepted regions, or how to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/pay.mdx
new file mode 100644
index 00000000..d36eb128
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/pay.mdx
@@ -0,0 +1,3 @@
+## How does AnyManager pay?
+
+AnyMind pays publishers a fixed share agreed before you start. The share itself is not public, and neither are payment timing, the minimum, methods, or currency.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/rules.mdx
new file mode 100644
index 00000000..dde9e59e
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does AnyManager set?
+
+Not public. Exclusivity, ad placement, content rules, contract length, and notice aren't on AnyMind's public pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/service.mdx
new file mode 100644
index 00000000..74c7607a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/service.mdx
@@ -0,0 +1,3 @@
+## What does AnyManager do for you?
+
+AnyManager offers header bidding, automatic price tuning, analytics, and tools for viewability and brand safety. Its dashboard and support levels are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx
index 7531ce64..76c49f9e 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Who is Carbon Ads for?
+
+Sites that want an ad built to match their design. Its terms aren't public, so contact BuySellAds to see if your site fits.
+
+## What's the difference between BuySellAds and Carbon Ads?
+
+BuySellAds is the company. Carbon Ads is one of its ad programs for publishers.
+
+## How can you check if a site uses Carbon Ads?
+
+Look for Carbon Ads in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Carbon Ads.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx
index 06a32587..ee82c07b 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx
@@ -1 +1 @@
-Publisher advertising programs including the Carbon Ads network. A site can use more than one ad network.
+Carbon Ads, from BuySellAds, places ads built to fit each site's design. Its joining rules and pay terms are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/joining.mdx
new file mode 100644
index 00000000..515a9ab5
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Carbon Ads?
+
+Not public. The Carbon Ads media kit doesn't give a traffic minimum, site rules, regions, or how to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/pay.mdx
new file mode 100644
index 00000000..53ded92c
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/pay.mdx
@@ -0,0 +1,3 @@
+## How does Carbon Ads pay?
+
+Not public. The media kit doesn't give the publisher share, when payments go out, the minimum, methods, or currency.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/rules.mdx
new file mode 100644
index 00000000..b1d32947
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Carbon Ads set?
+
+Carbon Ads builds each ad unit to fit the publisher's design. Exclusivity, content rules, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/service.mdx
new file mode 100644
index 00000000..1b5aa8de
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/service.mdx
@@ -0,0 +1,3 @@
+## What does Carbon Ads do for you?
+
+You get performance tracking, analytics, monthly reports, and suggestions to improve results.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx
index 7531ce64..4a86b163 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is Clickio a fit for a site outside the US?
+
+It can be. Clickio sets its traffic bar by visitor region, with rules for readers across Europe, Latin America, and elsewhere. The bar is higher outside its main markets.
+
+## What if your traffic is below Clickio's bar?
+
+Clickio may still take your site if it already earns enough from ads. Revenue counts as well as pageviews, so mention your earnings when you apply.
+
+## Can you keep other ad partners with Clickio?
+
+Clickio's entry rules don't say it must be your only partner. Confirm what your agreement says before you add others.
+
+## How can you check if a site uses Clickio?
+
+Look for Clickio in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Clickio.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx
index 3f9b5f4c..7d4e8c71 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx
@@ -1 +1 @@
-Publisher monetization platform. A site can use more than one ad network.
+Clickio is an ad platform that can run a site's ads as a fully managed service. It takes sites from many regions, with a traffic bar that depends on where readers are.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/joining.mdx
new file mode 100644
index 00000000..0958b2e4
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Clickio?
+
+The traffic Clickio wants depends on where your visitors are. Sites with readers in the UK, France, the US, Canada, or Australia have the lowest bar, and other regions need more. A smaller site can still qualify if it earns enough from ads.
+
+You need six months of history, original content you have rights to, and good standing with Google's policies. Adult, shocking, and racially intolerant content isn't allowed. Clickio also lists sanctioned countries it can't work with, including Cuba, Iran, Syria, Russia, and Sudan.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/pay.mdx
new file mode 100644
index 00000000..7ac79a63
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/pay.mdx
@@ -0,0 +1,5 @@
+## How does Clickio pay?
+
+Clickio bills monthly and pays on Net 60, around the 25th to 30th, two months after the month you earn. The minimum depends on how you're paid, and some methods can pay in your local currency.
+
+Its standard share is not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/rules.mdx
new file mode 100644
index 00000000..0279c4f8
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Clickio set?
+
+Clickio's entry rules don't say it must be your only ad partner, and they don't set a notice period. Clickio Prism, its CDN product, has separate rules.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/service.mdx
new file mode 100644
index 00000000..4feeefbf
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/service.mdx
@@ -0,0 +1,3 @@
+## What does Clickio do for you?
+
+The fully managed service gives you real-time reports, an account manager, and technical and ad operations support. Unified Reports puts your Clickio, AdSense, Ad Manager, and Google Analytics numbers in one place, and you can export them.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx
index 7531ce64..d122ea08 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## What is Concert?
+
+Concert is a publisher-led advertising marketplace run by Vox Media. The terms on its site are for advertisers who buy ads through it.
+
+## Can any site join Concert?
+
+Concert doesn't publish publisher rules or an application form. Contact its team to ask.
+
+## How can you check if a site uses Concert?
+
+Look for Concert in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Concert.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx
index 0cee2ade..360d09bf 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx
@@ -1 +1 @@
-Vox Media publisher-led advertising marketplace. A site can use more than one ad network.
+Concert is a publisher-led advertising marketplace run by Vox Media. Its public terms are for advertisers, and its publisher terms are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/joining.mdx
new file mode 100644
index 00000000..6f0bfbd4
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Concert?
+
+Not public. The Concert terms we found cover advertisers buying ads, not publishers joining. Traffic, content rules, regions, and how to apply are not published.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/pay.mdx
new file mode 100644
index 00000000..4dedddb6
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/pay.mdx
@@ -0,0 +1,3 @@
+## How does Concert pay publishers?
+
+Not public. Concert doesn't publish a share, timing, minimum, methods, or currency for publishers.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/rules.mdx
new file mode 100644
index 00000000..123c777d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Concert set?
+
+Not public. Concert's advertiser terms don't cover a publisher's exclusivity, ad layout, content rules, contract length, or notice.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/service.mdx
new file mode 100644
index 00000000..e298c1a3
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/service.mdx
@@ -0,0 +1,3 @@
+## What does Concert do for publishers?
+
+Not public. Concert doesn't describe a publisher dashboard, reports, or support on its public pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx
index 7531ce64..11f53005 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is EthicalAds a fit for your site?
+
+It suits developer and open source sites that want one clean ad instead of many. It won't fit a page that needs several ad slots, because EthicalAds must be the only ad on that page.
+
+## Can you run EthicalAds with another network?
+
+Yes, as long as they never share a page. EthicalAds lets you split your visitors with another network, or set a fallback ad.
+
+## Where should the EthicalAds ad go?
+
+Above the fold, on both desktop and mobile. That's what the policy asks for, along with a minimum click-through rate.
+
+## How can you check if a site uses EthicalAds?
+
+Look for EthicalAds in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to EthicalAds.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx
index b38e0d94..9cc387c0 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx
@@ -1 +1 @@
-Privacy-focused advertising network for developer and open source websites. A site can use more than one ad network.
+EthicalAds is a privacy-focused ad network for developer and open source sites. It suits sites that can give its one ad a clear spot near the top of the page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/joining.mdx
new file mode 100644
index 00000000..2d5dc0fa
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join EthicalAds?
+
+There's no traffic minimum. You need an approved account, and your site has to meet the publisher policy.
+
+The ad should sit above the fold on desktop and mobile. EthicalAds expects a click-through rate of at least 0.1%.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/pay.mdx
new file mode 100644
index 00000000..d530145a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/pay.mdx
@@ -0,0 +1,3 @@
+## How does EthicalAds pay?
+
+EthicalAds pays monthly once your balance reaches the minimum, aiming to pay by the 15th. Methods include Stripe Connect bank transfer, PayPal, OpenCollective, and GitHub Sponsors.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/rules.mdx
new file mode 100644
index 00000000..572f0717
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does EthicalAds set?
+
+Don't show another ad on the same page as an EthicalAds ad. You can use a fallback ad, or show EthicalAds to some visitors and another network to others.
+
+Content that breaks the policy can get your ads turned off. The policy doesn't set a contract term or notice period.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/service.mdx
new file mode 100644
index 00000000..56209111
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/service.mdx
@@ -0,0 +1,3 @@
+## What does EthicalAds do for you?
+
+The publisher policy explains setup and the rules. Dashboard features and support response times are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx
index 7531ce64..8869deec 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Can a small site still join Ezoic?
+
+Not as a new site on the main program. New sites below the traffic rule can apply to the Ezoic Incubator instead. Sites already earning with Ezoic before the rule changed can stay.
+
+## Why do Ezoic's payment dates look different in different places?
+
+Ezoic's legal terms and its payment help page describe the timing differently. The terms are the contract, so we show those. Ask Ezoic to confirm your schedule before you count on a date.
+
+## What content does Ezoic accept?
+
+Original content that follows AdSense policies, with visitors who are real people rather than bots.
+
+## How can you check if a site uses Ezoic?
+
+Look for Ezoic in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Ezoic.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx
index 66637bdf..7c5efc69 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx
@@ -1 +1 @@
-Website monetization tools for digital publishers. A site can use more than one ad network.
+Ezoic runs ads and site tools for website publishers. New sites need large traffic to join, and smaller sites can apply to its Incubator.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/joining.mdx
new file mode 100644
index 00000000..bf0fbba3
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Ezoic?
+
+Ezoic set its current traffic rule for new sites in February 2026. Sites already earning with Ezoic before February 19, 2026 can keep going under the old rule. Smaller sites can apply to the Ezoic Incubator.
+
+Your content must be original and follow AdSense policies, and your traffic must come from real people.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/pay.mdx
new file mode 100644
index 00000000..36ea27da
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/pay.mdx
@@ -0,0 +1,5 @@
+## How does Ezoic pay?
+
+Ezoic's terms say you get at least 82% of net ad revenue, unless you've signed a different agreement. They say payment comes about 35 days after the advertiser pays Ezoic.
+
+Ezoic's payment help page describes a Net 30 cycle instead. We go by the legal terms, but ask Ezoic which applies to you.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/rules.mdx
new file mode 100644
index 00000000..b2377a6b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Ezoic set?
+
+Your content must follow AdSense policies. Exclusivity, ad placement controls, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/service.mdx
new file mode 100644
index 00000000..2573c8a6
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/service.mdx
@@ -0,0 +1,3 @@
+## What does Ezoic do for you?
+
+Ezoic offers a dashboard and publisher tools through its site. Exact reporting and support levels are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx
index 7531ce64..33284ead 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is Freestar a fit for a growing site?
+
+Only once it's large. Freestar usually works with high-traffic sites, and it checks where your readers are. A site with most readers outside the US, UK, Canada, New Zealand, and Australia won't meet the rule, even if it's big.
+
+## How long does it take to start with Freestar?
+
+Plan for several weeks. An account manager and a technical team guide the setup.
+
+## What does Freestar check before it accepts you?
+
+Your content, your standing with ad providers, and where your traffic comes from. It may ask for GA4 access and your traffic and revenue history.
+
+## How can you check if a site uses Freestar?
+
+Look for Freestar in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Freestar.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx
index b283e52a..ab4e5a15 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx
@@ -1 +1 @@
-Publisher advertising and monetization platform. A site can use more than one ad network.
+Freestar is a managed ad service for large, brand-safe sites. It wants a big share of readers in the US, UK, Canada, New Zealand, or Australia.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/joining.mdx
new file mode 100644
index 00000000..07af2203
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/joining.mdx
@@ -0,0 +1,7 @@
+## Who can join Freestar?
+
+Freestar's traffic rule is what it usually asks for, not a strict cutoff, so ask its team if you're close. At least 50% of your traffic must come from the US, UK, Canada, New Zealand, or Australia.
+
+Your content must be original and made for people, and you need good standing with major ad providers. Explicit sexual content, piracy, violence, hate speech, and illegal drugs aren't allowed.
+
+During review, Freestar may ask for GA4 access and your past traffic and revenue.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/pay.mdx
new file mode 100644
index 00000000..3d709ff0
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/pay.mdx
@@ -0,0 +1,3 @@
+## How does Freestar pay?
+
+Not public. Freestar doesn't publish its share, payment timing, minimum, methods, or currency.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/rules.mdx
new file mode 100644
index 00000000..7d0eb83a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Freestar set?
+
+Not public. Freestar doesn't publish exclusivity, ad placement controls, contract length, or notice.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/service.mdx
new file mode 100644
index 00000000..ef67472e
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/service.mdx
@@ -0,0 +1,3 @@
+## What does Freestar do for you?
+
+You get a dedicated account manager, a technical team, and publisher consultants. Freestar says onboarding commonly takes three to six weeks.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx
index 7531ce64..4c36ee4a 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## What is a supply-side platform?
+
+A supply-side platform (SSP) sells a publisher's ad space to many buyers at once, in real time.
+
+## How do you join Geniee SSP?
+
+Geniee doesn't publish an application route for publishers. Contact Geniee to ask.
+
+## How can you check if a site uses Geniee SSP?
+
+Look for Geniee SSP in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Geniee SSP.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx
index f3e35ac7..3a8ff7be 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx
@@ -1 +1 @@
-Publisher supply-side platform operated by Geniee. A site can use more than one ad network.
+Geniee SSP is a supply-side platform run by Geniee. It sells publishers' ad space to buyers, and its publisher rules and terms are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/joining.mdx
new file mode 100644
index 00000000..8259998f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Geniee SSP?
+
+Not public. Geniee doesn't publish a traffic minimum, site rules, accepted regions, or how to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/pay.mdx
new file mode 100644
index 00000000..9778233a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/pay.mdx
@@ -0,0 +1,3 @@
+## How does Geniee SSP pay?
+
+Not public. The share, payment timing, minimum, methods, and currency aren't on Geniee's public pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/rules.mdx
new file mode 100644
index 00000000..3f137ecf
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Geniee SSP set?
+
+Not public. Exclusivity, ad placement, content rules, contract length, and notice aren't on Geniee's public pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/service.mdx
new file mode 100644
index 00000000..6dcc673b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/service.mdx
@@ -0,0 +1,3 @@
+## What does Geniee SSP do for you?
+
+Not public. Geniee doesn't describe its publisher dashboard, reports, or support.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx
index 7531ce64..6e1527d3 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is AdSense a good first ad network?
+
+For many new sites it's the easiest start. There's no traffic minimum, and you set it up yourself in your AdSense account.
+
+## Can you use AdSense with another network?
+
+AdSense doesn't ask to be your only ad partner. Some managed networks do, so check their rules before you add one.
+
+## Why do you earn less than the share Google shows?
+
+The share applies after the advertiser platform fee. For ads bought through Google Ads, your cut of what the advertiser paid ends up lower than the headline share.
+
+## How can you check if a site uses Google AdSense?
+
+Look for Google AdSense in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Google AdSense.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx
index b1ee6b14..8e8db26c 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx
@@ -1 +1 @@
-Google advertising program for website publishers. A site can use more than one ad network.
+Google AdSense is Google's ad program for website publishers. It has no traffic minimum, so it suits new and small sites.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/joining.mdx
new file mode 100644
index 00000000..d861bd81
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Google AdSense?
+
+You must be at least 18, own original content that follows AdSense policies, and be able to edit your site's HTML. There's no traffic minimum.
+
+You sign up in an AdSense account. Whether AdSense is open to you, and how you can be paid, depends on your country.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/pay.mdx
new file mode 100644
index 00000000..8c4754df
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/pay.mdx
@@ -0,0 +1,5 @@
+## How does Google AdSense pay?
+
+For ads on your content, you get 80% after the advertiser platform fee. When the ad comes from Google Ads, that works out to about 68% of what the advertiser pays. Other AdSense products have different shares.
+
+Google pays once a month, usually between the 21st and 26th, when your balance passes the minimum and there's no hold on your account. The minimum is set for each currency, and payment methods depend on your country.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/rules.mdx
new file mode 100644
index 00000000..4bbe9716
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Google AdSense set?
+
+Where you place ads must follow the AdSense Program Policies, which also list the content that isn't allowed. AdSense doesn't ask to be your only ad partner.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/service.mdx
new file mode 100644
index 00000000..ba66eadb
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/service.mdx
@@ -0,0 +1,3 @@
+## What does Google AdSense do for you?
+
+You get reports in your AdSense account, and help through the AdSense Help Center and its policy pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/index.ts b/app/(site)/(directories)/ad-networks/[slug]/content/index.ts
index 598b32e3..73d8b81c 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/index.ts
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/index.ts
@@ -1,139 +1,322 @@
-import type { ComponentType } from "react";
+import type { MDXContent } from "mdx/types";
import Intro1 from "./adapex/intro.mdx";
import Article1 from "./adapex/article.mdx";
+import Joining1 from "./adapex/joining.mdx";
+import Pay1 from "./adapex/pay.mdx";
+import Rules1 from "./adapex/rules.mdx";
+import Service1 from "./adapex/service.mdx";
import Intro2 from "./aditude/intro.mdx";
import Article2 from "./aditude/article.mdx";
+import Joining2 from "./aditude/joining.mdx";
+import Pay2 from "./aditude/pay.mdx";
+import Rules2 from "./aditude/rules.mdx";
+import Service2 from "./aditude/service.mdx";
import Intro3 from "./adnimation/intro.mdx";
import Article3 from "./adnimation/article.mdx";
+import Joining3 from "./adnimation/joining.mdx";
+import Pay3 from "./adnimation/pay.mdx";
+import Rules3 from "./adnimation/rules.mdx";
+import Service3 from "./adnimation/service.mdx";
import Intro4 from "./adpushup/intro.mdx";
import Article4 from "./adpushup/article.mdx";
+import Joining4 from "./adpushup/joining.mdx";
+import Pay4 from "./adpushup/pay.mdx";
+import Rules4 from "./adpushup/rules.mdx";
+import Service4 from "./adpushup/service.mdx";
import Intro5 from "./amazon-publisher-services/intro.mdx";
import Article5 from "./amazon-publisher-services/article.mdx";
+import Joining5 from "./amazon-publisher-services/joining.mdx";
+import Pay5 from "./amazon-publisher-services/pay.mdx";
+import Rules5 from "./amazon-publisher-services/rules.mdx";
+import Service5 from "./amazon-publisher-services/service.mdx";
import Intro6 from "./anymind-anymanager/intro.mdx";
import Article6 from "./anymind-anymanager/article.mdx";
+import Joining6 from "./anymind-anymanager/joining.mdx";
+import Pay6 from "./anymind-anymanager/pay.mdx";
+import Rules6 from "./anymind-anymanager/rules.mdx";
+import Service6 from "./anymind-anymanager/service.mdx";
import Intro7 from "./buysellads-carbon-ads/intro.mdx";
import Article7 from "./buysellads-carbon-ads/article.mdx";
+import Joining7 from "./buysellads-carbon-ads/joining.mdx";
+import Pay7 from "./buysellads-carbon-ads/pay.mdx";
+import Rules7 from "./buysellads-carbon-ads/rules.mdx";
+import Service7 from "./buysellads-carbon-ads/service.mdx";
import Intro8 from "./clickio/intro.mdx";
import Article8 from "./clickio/article.mdx";
+import Joining8 from "./clickio/joining.mdx";
+import Pay8 from "./clickio/pay.mdx";
+import Rules8 from "./clickio/rules.mdx";
+import Service8 from "./clickio/service.mdx";
import Intro9 from "./concert/intro.mdx";
import Article9 from "./concert/article.mdx";
+import Joining9 from "./concert/joining.mdx";
+import Pay9 from "./concert/pay.mdx";
+import Rules9 from "./concert/rules.mdx";
+import Service9 from "./concert/service.mdx";
import Intro10 from "./ethicalads/intro.mdx";
import Article10 from "./ethicalads/article.mdx";
+import Joining10 from "./ethicalads/joining.mdx";
+import Pay10 from "./ethicalads/pay.mdx";
+import Rules10 from "./ethicalads/rules.mdx";
+import Service10 from "./ethicalads/service.mdx";
import Intro11 from "./ezoic/intro.mdx";
import Article11 from "./ezoic/article.mdx";
+import Joining11 from "./ezoic/joining.mdx";
+import Pay11 from "./ezoic/pay.mdx";
+import Rules11 from "./ezoic/rules.mdx";
+import Service11 from "./ezoic/service.mdx";
import Intro12 from "./freestar/intro.mdx";
import Article12 from "./freestar/article.mdx";
+import Joining12 from "./freestar/joining.mdx";
+import Pay12 from "./freestar/pay.mdx";
+import Rules12 from "./freestar/rules.mdx";
+import Service12 from "./freestar/service.mdx";
import Intro13 from "./geniee-ssp/intro.mdx";
import Article13 from "./geniee-ssp/article.mdx";
+import Joining13 from "./geniee-ssp/joining.mdx";
+import Pay13 from "./geniee-ssp/pay.mdx";
+import Rules13 from "./geniee-ssp/rules.mdx";
+import Service13 from "./geniee-ssp/service.mdx";
import Intro14 from "./google-adsense/intro.mdx";
import Article14 from "./google-adsense/article.mdx";
+import Joining14 from "./google-adsense/joining.mdx";
+import Pay14 from "./google-adsense/pay.mdx";
+import Rules14 from "./google-adsense/rules.mdx";
+import Service14 from "./google-adsense/service.mdx";
import Intro15 from "./infolinks/intro.mdx";
import Article15 from "./infolinks/article.mdx";
+import Joining15 from "./infolinks/joining.mdx";
+import Pay15 from "./infolinks/pay.mdx";
+import Rules15 from "./infolinks/rules.mdx";
+import Service15 from "./infolinks/service.mdx";
import Intro16 from "./journey-by-mediavine/intro.mdx";
import Article16 from "./journey-by-mediavine/article.mdx";
+import Joining16 from "./journey-by-mediavine/joining.mdx";
+import Pay16 from "./journey-by-mediavine/pay.mdx";
+import Rules16 from "./journey-by-mediavine/rules.mdx";
+import Service16 from "./journey-by-mediavine/service.mdx";
import Intro17 from "./media-net/intro.mdx";
import Article17 from "./media-net/article.mdx";
+import Joining17 from "./media-net/joining.mdx";
+import Pay17 from "./media-net/pay.mdx";
+import Rules17 from "./media-net/rules.mdx";
+import Service17 from "./media-net/service.mdx";
import Intro18 from "./mediavine/intro.mdx";
import Article18 from "./mediavine/article.mdx";
+import Joining18 from "./mediavine/joining.mdx";
+import Pay18 from "./mediavine/pay.mdx";
+import Rules18 from "./mediavine/rules.mdx";
+import Service18 from "./mediavine/service.mdx";
import Intro19 from "./mgid/intro.mdx";
import Article19 from "./mgid/article.mdx";
+import Joining19 from "./mgid/joining.mdx";
+import Pay19 from "./mgid/pay.mdx";
+import Rules19 from "./mgid/rules.mdx";
+import Service19 from "./mgid/service.mdx";
import Intro20 from "./monetizemore-pubguru/intro.mdx";
import Article20 from "./monetizemore-pubguru/article.mdx";
+import Joining20 from "./monetizemore-pubguru/joining.mdx";
+import Pay20 from "./monetizemore-pubguru/pay.mdx";
+import Rules20 from "./monetizemore-pubguru/rules.mdx";
+import Service20 from "./monetizemore-pubguru/service.mdx";
import Intro21 from "./monumetric/intro.mdx";
import Article21 from "./monumetric/article.mdx";
+import Joining21 from "./monumetric/joining.mdx";
+import Pay21 from "./monumetric/pay.mdx";
+import Rules21 from "./monumetric/rules.mdx";
+import Service21 from "./monumetric/service.mdx";
import Intro22 from "./newor-media/intro.mdx";
import Article22 from "./newor-media/article.mdx";
+import Joining22 from "./newor-media/joining.mdx";
+import Pay22 from "./newor-media/pay.mdx";
+import Rules22 from "./newor-media/rules.mdx";
+import Service22 from "./newor-media/service.mdx";
import Intro23 from "./nitropay/intro.mdx";
import Article23 from "./nitropay/article.mdx";
+import Joining23 from "./nitropay/joining.mdx";
+import Pay23 from "./nitropay/pay.mdx";
+import Rules23 from "./nitropay/rules.mdx";
+import Service23 from "./nitropay/service.mdx";
import Intro24 from "./opti-digital/intro.mdx";
import Article24 from "./opti-digital/article.mdx";
+import Joining24 from "./opti-digital/joining.mdx";
+import Pay24 from "./opti-digital/pay.mdx";
+import Rules24 from "./opti-digital/rules.mdx";
+import Service24 from "./opti-digital/service.mdx";
import Intro25 from "./playwire/intro.mdx";
import Article25 from "./playwire/article.mdx";
+import Joining25 from "./playwire/joining.mdx";
+import Pay25 from "./playwire/pay.mdx";
+import Rules25 from "./playwire/rules.mdx";
+import Service25 from "./playwire/service.mdx";
import Intro26 from "./pubfuture/intro.mdx";
import Article26 from "./pubfuture/article.mdx";
+import Joining26 from "./pubfuture/joining.mdx";
+import Pay26 from "./pubfuture/pay.mdx";
+import Rules26 from "./pubfuture/rules.mdx";
+import Service26 from "./pubfuture/service.mdx";
import Intro27 from "./pubgalaxy/intro.mdx";
import Article27 from "./pubgalaxy/article.mdx";
+import Joining27 from "./pubgalaxy/joining.mdx";
+import Pay27 from "./pubgalaxy/pay.mdx";
+import Rules27 from "./pubgalaxy/rules.mdx";
+import Service27 from "./pubgalaxy/service.mdx";
import Intro28 from "./publift/intro.mdx";
import Article28 from "./publift/article.mdx";
+import Joining28 from "./publift/joining.mdx";
+import Pay28 from "./publift/pay.mdx";
+import Rules28 from "./publift/rules.mdx";
+import Service28 from "./publift/service.mdx";
import Intro29 from "./publir/intro.mdx";
import Article29 from "./publir/article.mdx";
+import Joining29 from "./publir/joining.mdx";
+import Pay29 from "./publir/pay.mdx";
+import Rules29 from "./publir/rules.mdx";
+import Service29 from "./publir/service.mdx";
import Intro30 from "./publisher-collective/intro.mdx";
import Article30 from "./publisher-collective/article.mdx";
+import Joining30 from "./publisher-collective/joining.mdx";
+import Pay30 from "./publisher-collective/pay.mdx";
+import Rules30 from "./publisher-collective/rules.mdx";
+import Service30 from "./publisher-collective/service.mdx";
import Intro31 from "./pubnation/intro.mdx";
import Article31 from "./pubnation/article.mdx";
+import Joining31 from "./pubnation/joining.mdx";
+import Pay31 from "./pubnation/pay.mdx";
+import Rules31 from "./pubnation/rules.mdx";
+import Service31 from "./pubnation/service.mdx";
import Intro32 from "./pubpower/intro.mdx";
import Article32 from "./pubpower/article.mdx";
+import Joining32 from "./pubpower/joining.mdx";
+import Pay32 from "./pubpower/pay.mdx";
+import Rules32 from "./pubpower/rules.mdx";
+import Service32 from "./pubpower/service.mdx";
import Intro33 from "./raptive/intro.mdx";
import Article33 from "./raptive/article.mdx";
+import Joining33 from "./raptive/joining.mdx";
+import Pay33 from "./raptive/pay.mdx";
+import Rules33 from "./raptive/rules.mdx";
+import Service33 from "./raptive/service.mdx";
import Intro34 from "./refinery89/intro.mdx";
import Article34 from "./refinery89/article.mdx";
+import Joining34 from "./refinery89/joining.mdx";
+import Pay34 from "./refinery89/pay.mdx";
+import Rules34 from "./refinery89/rules.mdx";
+import Service34 from "./refinery89/service.mdx";
import Intro35 from "./revcontent/intro.mdx";
import Article35 from "./revcontent/article.mdx";
+import Joining35 from "./revcontent/joining.mdx";
+import Pay35 from "./revcontent/pay.mdx";
+import Rules35 from "./revcontent/rules.mdx";
+import Service35 from "./revcontent/service.mdx";
import Intro36 from "./setupad/intro.mdx";
import Article36 from "./setupad/article.mdx";
+import Joining36 from "./setupad/joining.mdx";
+import Pay36 from "./setupad/pay.mdx";
+import Rules36 from "./setupad/rules.mdx";
+import Service36 from "./setupad/service.mdx";
import Intro37 from "./she-media/intro.mdx";
import Article37 from "./she-media/article.mdx";
+import Joining37 from "./she-media/joining.mdx";
+import Pay37 from "./she-media/pay.mdx";
+import Rules37 from "./she-media/rules.mdx";
+import Service37 from "./she-media/service.mdx";
import Intro38 from "./snack-media/intro.mdx";
import Article38 from "./snack-media/article.mdx";
-import Intro39 from "./snigel/intro.mdx";
-import Article39 from "./snigel/article.mdx";
+import Joining38 from "./snack-media/joining.mdx";
+import Pay38 from "./snack-media/pay.mdx";
+import Rules38 from "./snack-media/rules.mdx";
+import Service38 from "./snack-media/service.mdx";
import Intro40 from "./sovrn/intro.mdx";
import Article40 from "./sovrn/article.mdx";
+import Joining40 from "./sovrn/joining.mdx";
+import Pay40 from "./sovrn/pay.mdx";
+import Rules40 from "./sovrn/rules.mdx";
+import Service40 from "./sovrn/service.mdx";
import Intro41 from "./sparteo/intro.mdx";
import Article41 from "./sparteo/article.mdx";
+import Joining41 from "./sparteo/joining.mdx";
+import Pay41 from "./sparteo/pay.mdx";
+import Rules41 from "./sparteo/rules.mdx";
+import Service41 from "./sparteo/service.mdx";
import Intro42 from "./taboola/intro.mdx";
import Article42 from "./taboola/article.mdx";
+import Joining42 from "./taboola/joining.mdx";
+import Pay42 from "./taboola/pay.mdx";
+import Rules42 from "./taboola/rules.mdx";
+import Service42 from "./taboola/service.mdx";
import Intro43 from "./teads/intro.mdx";
import Article43 from "./teads/article.mdx";
+import Joining43 from "./teads/joining.mdx";
+import Pay43 from "./teads/pay.mdx";
+import Rules43 from "./teads/rules.mdx";
+import Service43 from "./teads/service.mdx";
import Intro44 from "./the-moneytizer/intro.mdx";
import Article44 from "./the-moneytizer/article.mdx";
+import Joining44 from "./the-moneytizer/joining.mdx";
+import Pay44 from "./the-moneytizer/pay.mdx";
+import Rules44 from "./the-moneytizer/rules.mdx";
+import Service44 from "./the-moneytizer/service.mdx";
import Intro45 from "./venatus/intro.mdx";
import Article45 from "./venatus/article.mdx";
+import Joining45 from "./venatus/joining.mdx";
+import Pay45 from "./venatus/pay.mdx";
+import Rules45 from "./venatus/rules.mdx";
+import Service45 from "./venatus/service.mdx";
-export const adNetworkContent: Record = {
- "adapex": { Intro: Intro1, Article: Article1 },
- "aditude": { Intro: Intro2, Article: Article2 },
- "adnimation": { Intro: Intro3, Article: Article3 },
- "adpushup": { Intro: Intro4, Article: Article4 },
- "amazon-publisher-services": { Intro: Intro5, Article: Article5 },
- "anymind-anymanager": { Intro: Intro6, Article: Article6 },
- "buysellads-carbon-ads": { Intro: Intro7, Article: Article7 },
- "clickio": { Intro: Intro8, Article: Article8 },
- "concert": { Intro: Intro9, Article: Article9 },
- "ethicalads": { Intro: Intro10, Article: Article10 },
- "ezoic": { Intro: Intro11, Article: Article11 },
- "freestar": { Intro: Intro12, Article: Article12 },
- "geniee-ssp": { Intro: Intro13, Article: Article13 },
- "google-adsense": { Intro: Intro14, Article: Article14 },
- "infolinks": { Intro: Intro15, Article: Article15 },
- "journey-by-mediavine": { Intro: Intro16, Article: Article16 },
- "media-net": { Intro: Intro17, Article: Article17 },
- "mediavine": { Intro: Intro18, Article: Article18 },
- "mgid": { Intro: Intro19, Article: Article19 },
- "monetizemore-pubguru": { Intro: Intro20, Article: Article20 },
- "monumetric": { Intro: Intro21, Article: Article21 },
- "newor-media": { Intro: Intro22, Article: Article22 },
- "nitropay": { Intro: Intro23, Article: Article23 },
- "opti-digital": { Intro: Intro24, Article: Article24 },
- "playwire": { Intro: Intro25, Article: Article25 },
- "pubfuture": { Intro: Intro26, Article: Article26 },
- "pubgalaxy": { Intro: Intro27, Article: Article27 },
- "publift": { Intro: Intro28, Article: Article28 },
- "publir": { Intro: Intro29, Article: Article29 },
- "publisher-collective": { Intro: Intro30, Article: Article30 },
- "pubnation": { Intro: Intro31, Article: Article31 },
- "pubpower": { Intro: Intro32, Article: Article32 },
- "raptive": { Intro: Intro33, Article: Article33 },
- "refinery89": { Intro: Intro34, Article: Article34 },
- "revcontent": { Intro: Intro35, Article: Article35 },
- "setupad": { Intro: Intro36, Article: Article36 },
- "she-media": { Intro: Intro37, Article: Article37 },
- "snack-media": { Intro: Intro38, Article: Article38 },
- "snigel": { Intro: Intro39, Article: Article39 },
- "sovrn": { Intro: Intro40, Article: Article40 },
- "sparteo": { Intro: Intro41, Article: Article41 },
- "taboola": { Intro: Intro42, Article: Article42 },
- "teads": { Intro: Intro43, Article: Article43 },
- "the-moneytizer": { Intro: Intro44, Article: Article44 },
- "venatus": { Intro: Intro45, Article: Article45 },
+/** Words for one network page. The four tabs are optional until a writer adds them. */
+export type AdNetworkContent = {
+ Intro: MDXContent;
+ Article: MDXContent;
+ Joining?: MDXContent;
+ Pay?: MDXContent;
+ Rules?: MDXContent;
+ Service?: MDXContent;
+};
+
+export const adNetworkContent: Record = {
+ "adapex": { Intro: Intro1, Article: Article1, Joining: Joining1, Pay: Pay1, Rules: Rules1, Service: Service1 },
+ "aditude": { Intro: Intro2, Article: Article2, Joining: Joining2, Pay: Pay2, Rules: Rules2, Service: Service2 },
+ "adnimation": { Intro: Intro3, Article: Article3, Joining: Joining3, Pay: Pay3, Rules: Rules3, Service: Service3 },
+ "adpushup": { Intro: Intro4, Article: Article4, Joining: Joining4, Pay: Pay4, Rules: Rules4, Service: Service4 },
+ "amazon-publisher-services": { Intro: Intro5, Article: Article5, Joining: Joining5, Pay: Pay5, Rules: Rules5, Service: Service5 },
+ "anymind-anymanager": { Intro: Intro6, Article: Article6, Joining: Joining6, Pay: Pay6, Rules: Rules6, Service: Service6 },
+ "buysellads-carbon-ads": { Intro: Intro7, Article: Article7, Joining: Joining7, Pay: Pay7, Rules: Rules7, Service: Service7 },
+ "clickio": { Intro: Intro8, Article: Article8, Joining: Joining8, Pay: Pay8, Rules: Rules8, Service: Service8 },
+ "concert": { Intro: Intro9, Article: Article9, Joining: Joining9, Pay: Pay9, Rules: Rules9, Service: Service9 },
+ "ethicalads": { Intro: Intro10, Article: Article10, Joining: Joining10, Pay: Pay10, Rules: Rules10, Service: Service10 },
+ "ezoic": { Intro: Intro11, Article: Article11, Joining: Joining11, Pay: Pay11, Rules: Rules11, Service: Service11 },
+ "freestar": { Intro: Intro12, Article: Article12, Joining: Joining12, Pay: Pay12, Rules: Rules12, Service: Service12 },
+ "geniee-ssp": { Intro: Intro13, Article: Article13, Joining: Joining13, Pay: Pay13, Rules: Rules13, Service: Service13 },
+ "google-adsense": { Intro: Intro14, Article: Article14, Joining: Joining14, Pay: Pay14, Rules: Rules14, Service: Service14 },
+ "infolinks": { Intro: Intro15, Article: Article15, Joining: Joining15, Pay: Pay15, Rules: Rules15, Service: Service15 },
+ "journey-by-mediavine": { Intro: Intro16, Article: Article16, Joining: Joining16, Pay: Pay16, Rules: Rules16, Service: Service16 },
+ "media-net": { Intro: Intro17, Article: Article17, Joining: Joining17, Pay: Pay17, Rules: Rules17, Service: Service17 },
+ "mediavine": { Intro: Intro18, Article: Article18, Joining: Joining18, Pay: Pay18, Rules: Rules18, Service: Service18 },
+ "mgid": { Intro: Intro19, Article: Article19, Joining: Joining19, Pay: Pay19, Rules: Rules19, Service: Service19 },
+ "monetizemore-pubguru": { Intro: Intro20, Article: Article20, Joining: Joining20, Pay: Pay20, Rules: Rules20, Service: Service20 },
+ "monumetric": { Intro: Intro21, Article: Article21, Joining: Joining21, Pay: Pay21, Rules: Rules21, Service: Service21 },
+ "newor-media": { Intro: Intro22, Article: Article22, Joining: Joining22, Pay: Pay22, Rules: Rules22, Service: Service22 },
+ "nitropay": { Intro: Intro23, Article: Article23, Joining: Joining23, Pay: Pay23, Rules: Rules23, Service: Service23 },
+ "opti-digital": { Intro: Intro24, Article: Article24, Joining: Joining24, Pay: Pay24, Rules: Rules24, Service: Service24 },
+ "playwire": { Intro: Intro25, Article: Article25, Joining: Joining25, Pay: Pay25, Rules: Rules25, Service: Service25 },
+ "pubfuture": { Intro: Intro26, Article: Article26, Joining: Joining26, Pay: Pay26, Rules: Rules26, Service: Service26 },
+ "pubgalaxy": { Intro: Intro27, Article: Article27, Joining: Joining27, Pay: Pay27, Rules: Rules27, Service: Service27 },
+ "publift": { Intro: Intro28, Article: Article28, Joining: Joining28, Pay: Pay28, Rules: Rules28, Service: Service28 },
+ "publir": { Intro: Intro29, Article: Article29, Joining: Joining29, Pay: Pay29, Rules: Rules29, Service: Service29 },
+ "publisher-collective": { Intro: Intro30, Article: Article30, Joining: Joining30, Pay: Pay30, Rules: Rules30, Service: Service30 },
+ "pubnation": { Intro: Intro31, Article: Article31, Joining: Joining31, Pay: Pay31, Rules: Rules31, Service: Service31 },
+ "pubpower": { Intro: Intro32, Article: Article32, Joining: Joining32, Pay: Pay32, Rules: Rules32, Service: Service32 },
+ "raptive": { Intro: Intro33, Article: Article33, Joining: Joining33, Pay: Pay33, Rules: Rules33, Service: Service33 },
+ "refinery89": { Intro: Intro34, Article: Article34, Joining: Joining34, Pay: Pay34, Rules: Rules34, Service: Service34 },
+ "revcontent": { Intro: Intro35, Article: Article35, Joining: Joining35, Pay: Pay35, Rules: Rules35, Service: Service35 },
+ "setupad": { Intro: Intro36, Article: Article36, Joining: Joining36, Pay: Pay36, Rules: Rules36, Service: Service36 },
+ "she-media": { Intro: Intro37, Article: Article37, Joining: Joining37, Pay: Pay37, Rules: Rules37, Service: Service37 },
+ "snack-media": { Intro: Intro38, Article: Article38, Joining: Joining38, Pay: Pay38, Rules: Rules38, Service: Service38 },
+ "sovrn": { Intro: Intro40, Article: Article40, Joining: Joining40, Pay: Pay40, Rules: Rules40, Service: Service40 },
+ "sparteo": { Intro: Intro41, Article: Article41, Joining: Joining41, Pay: Pay41, Rules: Rules41, Service: Service41 },
+ "taboola": { Intro: Intro42, Article: Article42, Joining: Joining42, Pay: Pay42, Rules: Rules42, Service: Service42 },
+ "teads": { Intro: Intro43, Article: Article43, Joining: Joining43, Pay: Pay43, Rules: Rules43, Service: Service43 },
+ "the-moneytizer": { Intro: Intro44, Article: Article44, Joining: Joining44, Pay: Pay44, Rules: Rules44, Service: Service44 },
+ "venatus": { Intro: Intro45, Article: Article45, Joining: Joining45, Pay: Pay45, Rules: Rules45, Service: Service45 },
};
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx
index 7531ce64..f1584836 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is Infolinks a fit for a small site?
+
+It can be. Infolinks is open to sites of any size, so it can work before you qualify for a managed network.
+
+## How current are Infolinks' public terms?
+
+The public agreement is years old. Ask Infolinks for the current version before you rely on its numbers.
+
+## How can you check if a site uses Infolinks?
+
+Look for Infolinks in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Infolinks.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx
index 4b722216..41990089 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx
@@ -1 +1 @@
-Publisher advertising platform with in-text ad products. A site can use more than one ad network.
+Infolinks runs in-text ads and other ad units for websites. Publishers of any size can apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/joining.mdx
new file mode 100644
index 00000000..f0c60dc2
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Infolinks?
+
+Any size of site can apply, and there's no traffic minimum. Infolinks reviews each site and may reject it.
+
+Illegal, negative, offensive, adult, obscene, and porn content isn't allowed. You sign up with a short web form, and review usually takes 48 hours.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/pay.mdx
new file mode 100644
index 00000000..c0af55b9
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/pay.mdx
@@ -0,0 +1,5 @@
+## How does Infolinks pay?
+
+The public agreement gives you 70% for In-Text ads and pays 45 working days after the month ends. Other ad formats have different shares and minimums.
+
+That agreement was last updated in 2015. If you sign a newer one, the newer terms count.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/rules.mdx
new file mode 100644
index 00000000..ac48f346
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Infolinks set?
+
+The old public agreement starts with a 30-day trial, then renews every 12 months. It can be ended on one business day's notice.
+
+Adult and illegal content is banned. Check the current terms for exclusivity and ad placement rights.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/service.mdx
new file mode 100644
index 00000000..7da40934
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/service.mdx
@@ -0,0 +1,3 @@
+## What does Infolinks do for you?
+
+You track earnings in the Publisher Center. You can change how the ads look and how many show.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx
index 7531ce64..d1c359ad 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## How is Journey different from Mediavine?
+
+Journey is for sites still growing, with a much lower bar to join. [Mediavine](/ad-networks/mediavine)'s main program is for sites that already earn steady ad revenue. Journey's pay and rules aren't public, so don't assume they match Mediavine's.
+
+## What does Mediavine check when you apply to Journey?
+
+Your traffic sources, the countries your visitors come from, and who your readers are. It also wants original, brand-safe content made by people.
+
+## How can you check if a site uses Journey by Mediavine?
+
+Look for Journey by Mediavine in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Journey by Mediavine.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx
index 8b6af17e..39953461 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx
@@ -1 +1 @@
-Mediavine ad management program for growing publishers. A site can use more than one ad network.
+Journey by Mediavine is Mediavine's ad program for smaller, growing sites. It suits bloggers who aren't ready for Mediavine's main program yet.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/joining.mdx
new file mode 100644
index 00000000..59e9a7e8
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Journey by Mediavine?
+
+Journey's traffic bar is low. Your content must be original, updated often, made by people, and brand safe, with an engaged audience.
+
+Mediavine looks at your traffic sources, visitor countries, and reader demographics. You need the Grow plugin on your site. Journey doesn't say which regions it accepts.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/pay.mdx
new file mode 100644
index 00000000..e77eaffe
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/pay.mdx
@@ -0,0 +1,3 @@
+## How does Journey by Mediavine pay?
+
+Not public. Journey's share, payment timing, minimum, methods, and currency aren't published. Mediavine's main program terms don't apply to Journey.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/rules.mdx
new file mode 100644
index 00000000..f7fbb649
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Journey by Mediavine set?
+
+Not public. Exclusivity, ad placement rules, contract length, and notice for Journey aren't published.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/service.mdx
new file mode 100644
index 00000000..830b3bda
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/service.mdx
@@ -0,0 +1,3 @@
+## What does Journey by Mediavine do for you?
+
+Journey works through the Grow plugin. Other service and reporting details are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx
index 7531ce64..5d6d16cd 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## What is a contextual ad?
+
+It's an ad picked for the words on the page, not for the person reading it. A page about hiking shows hiking ads.
+
+## Who is Media.net a fit for?
+
+Sites you own and run, with real, unpaid traffic. Media.net checks for bought or invalid traffic, and too much of it can get your account suspended.
+
+## How can you check if a site uses Media.net?
+
+Look for Media.net in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Media.net.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx
index 1ffaadd6..8757ff47 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx
@@ -1 +1 @@
-Publisher ad monetization platform. A site can use more than one ad network.
+Media.net runs a contextual advertising program for publishers, showing ads based on what each page is about. Publishers apply to join, and the rules are in its public agreement.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/joining.mdx
new file mode 100644
index 00000000..d3b3ad7d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Media.net?
+
+Media.net doesn't publish a traffic minimum or accepted regions. Its agreement covers sites you own, run, or control, and you apply through its publisher program.
+
+Its inventory policy bans invalid or bought traffic above acceptable levels. Your ad requests and site details must be accurate.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/pay.mdx
new file mode 100644
index 00000000..d2adf0bf
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/pay.mdx
@@ -0,0 +1,3 @@
+## How does Media.net pay?
+
+Media.net pays monthly on Net 30 terms. Your pay is based on the valid click revenue it receives, minus its commission and costs. The share, minimum, methods, and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/rules.mdx
new file mode 100644
index 00000000..5016400e
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Media.net set?
+
+You agree to keep Media.net's confidential information private. Media.net can suspend or end your account for policy problems.
+
+The agreement doesn't state an exclusivity rule, placement rules, or a fixed contract term.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/service.mdx
new file mode 100644
index 00000000..68299755
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/service.mdx
@@ -0,0 +1,3 @@
+## What does Media.net do for you?
+
+The agreement explains how the program runs. Dashboard, reporting, and support levels are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx
index 7531ce64..47eefbbc 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx
@@ -1 +1,19 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## How do you join Mediavine?
+
+You apply with your site, and Mediavine reviews your ad revenue, traffic, and content against the rules here. If you're approved, you add their script and their ads.txt lines, and they set up and run the ads for you.
+
+## What if your site is too small for Mediavine?
+
+Mediavine runs a second program, [Journey](/ad-networks/journey-by-mediavine), for smaller sites. It has a lower bar to join and the same company behind it. Journey's page lists its rules and the sites that use it.
+
+## What does the pay day mean?
+
+Mediavine pays each month's earnings a set number of days after the month ends. Earnings from January arrive a couple of months later, not at the start of February. Plan for that gap if you switch from a network that pays faster.
+
+## Can you run other ads with Mediavine?
+
+Mediavine's help center lists its rules on ad placement and other ad partners. Affiliate links and sponsored posts are usually separate, but check the current rules with Mediavine before you sign.
+
+## How can you check if a site uses Mediavine?
+
+Look for Mediavine in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker).
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx
index e0fb65ff..4c29fb65 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx
@@ -1 +1 @@
-Ad management for publishers and creators. A site can use more than one ad network.
+Mediavine runs the ads for food, travel, home, and other content sites.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/joining.mdx
new file mode 100644
index 00000000..da15ec41
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Mediavine?
+
+Mediavine Official is for sites that already earn steady ad revenue. When you apply, Mediavine reviews both your traffic and your content.
+
+Your content needs to be original, high quality, and evergreen. If your site is still growing, [Journey](/ad-networks/journey-by-mediavine) is Mediavine's separate program for newer sites.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/pay.mdx
new file mode 100644
index 00000000..6a61560a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/pay.mdx
@@ -0,0 +1,5 @@
+## How does Mediavine pay?
+
+Each month's earnings are paid a set number of days after that month ends, on a fixed day of the month or the next business day. Your first payment arrives a couple of months after you start.
+
+The minimum payout depends on how you're paid, and some international methods need a higher balance. The currency you're paid in depends on the method you pick.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/rules.mdx
new file mode 100644
index 00000000..7b956494
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Mediavine set?
+
+Mediavine says there's no long-term contract. You control your ad settings yourself from the dashboard.
+
+Its rules on content and ad placement are in the [Mediavine help center](https://help.mediavine.com/).
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/service.mdx
new file mode 100644
index 00000000..4df2e262
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/service.mdx
@@ -0,0 +1,5 @@
+## What does Mediavine do for you?
+
+Your dashboard shows earnings by traffic source, country, device, page, and ad unit, with gross and net revenue side by side.
+
+Support is open every day of the week through the help center and the publisher community. Tools include Grow and the ad management settings.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx
index 7531ce64..40ba4da4 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Why does MGID's share depend on the offer?
+
+MGID sets pay in each publisher's order, not in one public rate card. The share in one regional offer may not be what you're offered, so compare the order you get.
+
+## Does MGID take small sites?
+
+It doesn't publish a single traffic minimum, and its Discover program says it has none. Your site still has to pass its quality rules.
+
+## How can you check if a site uses MGID?
+
+Look for MGID in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to MGID.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx
index 2dfcb12c..dd61f047 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx
@@ -1 +1 @@
-Native advertising and website monetization platform for publishers. A site can use more than one ad network.
+MGID is a native advertising platform that helps publishers earn from their sites. Its pay terms depend on the offer you sign.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/joining.mdx
new file mode 100644
index 00000000..2ea8da0f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join MGID?
+
+MGID doesn't publish one traffic minimum or region rule. Your site has to meet its traffic quality and content rules. You apply through its publisher sign-up or contact page.
+
+MGID's separate Google Discover offer says it has no traffic minimum, but that applies only to that program.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/pay.mdx
new file mode 100644
index 00000000..2c44e9c4
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/pay.mdx
@@ -0,0 +1,5 @@
+## How does MGID pay?
+
+A regional MGID offer lists a 65% share, Net 30 payment, and minimums from $50 to $1,000 depending on the method. That's one offer, not a rate for everyone.
+
+MGID's terms say your pay follows your account and order terms and can be adjusted. The order you sign is what counts.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/rules.mdx
new file mode 100644
index 00000000..bf926755
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does MGID set?
+
+The terms cover invalid traffic, how content and ads are shown, and allowed use. They don't set one exclusivity rule or contract length for everyone.
+
+A split mentioned in the terms is for an anti-adblock add-on, not the main service.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/service.mdx
new file mode 100644
index 00000000..82ae8462
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/service.mdx
@@ -0,0 +1,3 @@
+## What does MGID do for you?
+
+The regional offer includes a dedicated account manager and round-the-clock support. Dashboard and reporting details are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx
index 7531ce64..3601facd 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## What's the difference between MonetizeMore and PubGuru?
+
+MonetizeMore is the company. PubGuru is its ad platform, and PubGuru Premium is the higher tier with extra tools and a support team.
+
+## Is PubGuru Premium a fit for a smaller site?
+
+Not yet. Premium is for sites with high ad revenue or traffic. Ask MonetizeMore what it offers below that level.
+
+## How can you check if a site uses MonetizeMore?
+
+Look for MonetizeMore in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to MonetizeMore.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx
index f081d581..2e68b199 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx
@@ -1 +1 @@
-Managed ad operations and PubGuru monetization tools for publishers. A site can use more than one ad network.
+MonetizeMore manages ads for publishers, and PubGuru is its ad platform. PubGuru Premium suits sites that already earn well from ads or get heavy traffic.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/joining.mdx
new file mode 100644
index 00000000..c0cff58a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join PubGuru Premium?
+
+PubGuru Premium needs either $5,000 in monthly ad revenue or 2 million monthly pageviews across your sites. MonetizeMore's FAQ mentions only the revenue rule, but the Premium page gives both.
+
+You start by contacting MonetizeMore. The Premium page doesn't list a location rule.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/pay.mdx
new file mode 100644
index 00000000..ac277ce3
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/pay.mdx
@@ -0,0 +1,5 @@
+## How does MonetizeMore pay?
+
+Not public. The Premium page doesn't give the share, payment schedule, minimum, methods, or currency.
+
+MonetizeMore's Traffic Cop product has its own payment terms, which don't cover PubGuru.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/rules.mdx
new file mode 100644
index 00000000..78a36584
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does PubGuru Premium set?
+
+Not public. Contract terms, exclusivity, placement limits, and notice aren't on the product page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/service.mdx
new file mode 100644
index 00000000..8642d700
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/service.mdx
@@ -0,0 +1,5 @@
+## What does PubGuru Premium do for you?
+
+Premium includes custom price floors, match rate automation, smart refresh, A/B testing, ad block recovery, premium demand, Open Bidding, and ads.txt management. A Partner Success Squad covers onboarding, ongoing success, and technical solutions.
+
+PubGuru Analytics gives you custom reports and checks for revenue gaps between your reports.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx
index 7531ce64..17a8f36c 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is Monumetric a fit for a new blog?
+
+It can be, if you write in English and your readers are mainly in the US, UK, Canada, or Australia. Propel is its program for smaller sites.
+
+## When does Monumetric pay you?
+
+A couple of months after you earn it, early in the month. Small balances carry over until they pass the minimum.
+
+## How can you check if a site uses Monumetric?
+
+Look for Monumetric in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Monumetric.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx
index 49365a91..696594c3 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx
@@ -1 +1 @@
-Managed advertising for website publishers. A site can use more than one ad network.
+Monumetric is a managed ad service for English-language sites. Its Propel program suits smaller blogs that are starting to grow.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/joining.mdx
new file mode 100644
index 00000000..e5da3dbd
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Monumetric?
+
+Your content must be in English, and at least 50% of your traffic must come from the US, UK, Canada, or Australia. Monumetric won't take content that harms its brand, such as porn, drugs, hate speech, or copyrighted material.
+
+You apply through its program page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/pay.mdx
new file mode 100644
index 00000000..63751522
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/pay.mdx
@@ -0,0 +1,5 @@
+## How does Monumetric pay?
+
+Monumetric pays on Net 60, within the first ten business days of the month. If your balance is under the minimum, it rolls into the next month.
+
+US publishers are paid by direct deposit, and international publishers by PayPal. Payoneer can count as a US bank account in some cases. The share and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/rules.mdx
new file mode 100644
index 00000000..a34a48e1
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Monumetric set?
+
+Not public. Exclusivity, ad layout controls, contract length, and notice aren't on its eligibility or payment pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/service.mdx
new file mode 100644
index 00000000..fbe4acb0
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/service.mdx
@@ -0,0 +1,3 @@
+## What does Monumetric do for you?
+
+Monumetric manages and tunes your ads for you. Dashboard and support details are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx
index 7531ce64..9e2f478e 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Which Newor Media program fits your site?
+
+Start with Elevate if your site is new, since it has no stated traffic bar. Catapult and Premier are for sites with more US traffic.
+
+## Why are there two payment terms for Newor Media?
+
+The FAQ and the platform agreement disagree. The signed agreement is your contract, so go by that, and ask Newor to confirm your dates.
+
+## Can you run other ads with Newor Media?
+
+Not other display networks on Elevate, which is exclusive. Read the agreement for your program before you add partners.
+
+## How can you check if a site uses Newor Media?
+
+Look for Newor Media in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Newor Media.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx
index d863be3d..825a0cfb 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx
@@ -1 +1 @@
-Publisher programs for managing and monetizing website ads. A site can use more than one ad network.
+Newor Media runs ads for publishers through a ladder of programs, from Elevate for new sites to Catapult and Premier for larger ones. It suits English-language sites on Google Ad Manager.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/joining.mdx
new file mode 100644
index 00000000..1d1f96ff
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Newor Media?
+
+Elevate, the entry program, has no stated pageview minimum. Your site must be in English, have a navigation bar, follow Google's ad policies, and have room for at least two ads per page. You need Google Ad Manager. Approval usually takes 24 to 48 hours.
+
+The larger programs set traffic rules: Catapult at 60,000 US pageviews a month, and Premier at 420,000.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/pay.mdx
new file mode 100644
index 00000000..863c7567
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/pay.mdx
@@ -0,0 +1,5 @@
+## How does Newor Media pay?
+
+The payment FAQ says Net 30, but the platform agreement says Net 45. If you've signed the agreement, its terms are the ones that count.
+
+Payment is in US dollars. The share and minimum are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/rules.mdx
new file mode 100644
index 00000000..e995137d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Newor Media set?
+
+The Elevate program is exclusive. The platform agreement starts with a 90-day term, then renews. To leave, give written notice at least 14 days before the term ends.
+
+OTT inventory is handled separately and isn't exclusive. Some premium private marketplace deals do need exclusive inventory. Your content must follow Google's policies.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/service.mdx
new file mode 100644
index 00000000..21a2062d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/service.mdx
@@ -0,0 +1,3 @@
+## What does Newor Media do for you?
+
+The dashboard shows real-time reports. Support is by ticket, and Newor aims to reply within 24 hours.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx
index 7531ce64..97e44284 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## How fast does NitroPay pay?
+
+Faster than most networks on this list. Its docs describe payment about a week after the month, with no minimum beyond the transaction fee.
+
+## How long should you test NitroPay?
+
+NitroPay recommends a testing period of a few months before you judge results.
+
+## How can you check if a site uses NitroPay?
+
+Look for NitroPay in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to NitroPay.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx
index af2ba7a8..c0e85b9b 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx
@@ -1 +1 @@
-Ad monetization for website publishers. A site can use more than one ad network.
+NitroPay is an ad platform for website publishers that pays out quickly after each month.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/joining.mdx
new file mode 100644
index 00000000..5d957cac
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join NitroPay?
+
+Not public. NitroPay doesn't publish a traffic minimum, content rules, accepted regions, or application steps. Contact its team to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/pay.mdx
new file mode 100644
index 00000000..2e71101d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/pay.mdx
@@ -0,0 +1,5 @@
+## How does NitroPay pay?
+
+NitroPay's docs say it pays on Net 7, with no minimum as long as your earnings cover the transaction fee. The base share, full list of methods, and currency are not public.
+
+A separate bonus page lists share increases for some agreements. That's a bonus, not the starting share.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/rules.mdx
new file mode 100644
index 00000000..57c07b0b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does NitroPay set?
+
+Not public. Contract terms and how much control you have over ads and layout aren't in its public docs.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/service.mdx
new file mode 100644
index 00000000..453bf3f8
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/service.mdx
@@ -0,0 +1,3 @@
+## What does NitroPay do for you?
+
+You can reach its success team by email or through the chat in your dashboard. NitroPay suggests a 90-day testing period. Full dashboard and report details aren't in its public docs.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx
index 7531ce64..74e6dc9b 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## What's the difference between Managed Inventory and Managed Account?
+
+Both are ways Opti Digital runs your Google Ad Manager setup. They're separate commercial deals with different payment timing. With Managed Account, Google pays its share of revenue on its own schedule.
+
+## Who is Opti Digital for?
+
+Publishers who want a partner to run Google Ad Manager. Its fit rules aren't public, so ask its team.
+
+## How can you check if a site uses Opti Digital?
+
+Look for Opti Digital in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Opti Digital.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx
index 3f9b5f4c..72ba0df0 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx
@@ -1 +1 @@
-Publisher monetization platform. A site can use more than one ad network.
+Opti Digital is a monetization platform that manages Google Ad Manager setups for publishers. It offers two setups, and each pays on different terms.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/joining.mdx
new file mode 100644
index 00000000..82652644
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Opti Digital?
+
+Not public. Opti Digital doesn't publish a traffic minimum, site rules, regions, or an application checklist. Contact its team to talk about fit.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/pay.mdx
new file mode 100644
index 00000000..18e7bd9a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/pay.mdx
@@ -0,0 +1,7 @@
+## How does Opti Digital pay?
+
+Opti Digital works on a revenue share, but the rate is not public. When you're paid depends on your setup.
+
+With Managed Inventory, Opti Digital pays on Net 45. With Managed Account, it pays Net 60 for the demand it handles, and Google pays its own revenue on its 25-day cycle. Your order sets the exact terms.
+
+The minimum, methods, and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/rules.mdx
new file mode 100644
index 00000000..7675e79f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Opti Digital set?
+
+Not public. Exclusivity, content rules, contract length, and notice aren't on its help pages. Managed Inventory and Managed Account are different commercial deals, so read the terms for yours.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/service.mdx
new file mode 100644
index 00000000..8304b2e5
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/service.mdx
@@ -0,0 +1,3 @@
+## What does Opti Digital do for you?
+
+The publisher knowledge base explains how payment flows, and invoices are created for you. Dashboard reports and support levels are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx
index 7531ce64..b5bfd452 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## How much traffic do you need for Playwire?
+
+Playwire's own pages give different answers, so check the Joining tab and ask its team. Quality matters too: it checks invalid traffic and your ad setup.
+
+## Can you run other networks with Playwire?
+
+No. Playwire says other ad network tags can't run with its script. Plan to move your ad slots over to it.
+
+## Who decides where Playwire's ads go?
+
+You do. Playwire recommends layouts, but publishers have the final say.
+
+## How can you check if a site uses Playwire?
+
+Look for Playwire in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Playwire.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx
index e45c693f..e6568ee6 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx
@@ -1 +1 @@
-Advertising monetization for independent web publishers. A site can use more than one ad network.
+Playwire is a managed ad service for web publishers and apps. It suits established sites with original content and clean traffic.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/joining.mdx
new file mode 100644
index 00000000..a0c2a41f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Playwire?
+
+Playwire's pages disagree on traffic. Its FAQ asks for 500,000 monthly pageviews, or 1,000 daily active users for an app. Its qualification page says publishers are typically advised to have 100,000 pageviews and 20% US traffic.
+
+You also need original content, invalid traffic at or below 7%, a privacy policy, HTTPS, and room for at least three ads per device. Apply through Playwire's onboarding form.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/pay.mdx
new file mode 100644
index 00000000..b04a739e
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/pay.mdx
@@ -0,0 +1,3 @@
+## How does Playwire pay?
+
+Playwire pays monthly on Net 60 terms and works on a revenue share. The share, minimum, methods, and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/rules.mdx
new file mode 100644
index 00000000..05150f16
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Playwire set?
+
+Playwire suggests ad layouts, but you have the final say. You can't run other ad network tags alongside the Playwire script. Contract length and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/service.mdx
new file mode 100644
index 00000000..4fba3dd6
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/service.mdx
@@ -0,0 +1,3 @@
+## What does Playwire do for you?
+
+You get a dashboard, campaign tracking, and dedicated teams for publisher success, yield, and technical support.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx
index 7531ce64..7a26dd21 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is PubFuture a fit for a small site?
+
+It may be. PubFuture doesn't publish a traffic minimum, so smaller sites can apply.
+
+## Can you control which ads PubFuture shows?
+
+Yes. PubFuture says you can block ads you don't want.
+
+## How can you check if a site uses PubFuture?
+
+Look for PubFuture in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubFuture.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx
index 48fc1bf9..a3215702 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx
@@ -1 +1 @@
-Header bidding and advertising monetization services for publishers. A site can use more than one ad network.
+PubFuture is a header bidding and ad monetization service for publishers. It doesn't publish a traffic minimum, so smaller sites can apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/joining.mdx
new file mode 100644
index 00000000..8eee674a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join PubFuture?
+
+PubFuture doesn't publish a traffic minimum, accepted regions, or a full content checklist. You apply through its sign-up page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/pay.mdx
new file mode 100644
index 00000000..f80164fb
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/pay.mdx
@@ -0,0 +1,3 @@
+## How does PubFuture pay?
+
+PubFuture pays on Net 30 terms once you pass the minimum. Its revenue share is not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/rules.mdx
new file mode 100644
index 00000000..fefac70d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does PubFuture set?
+
+You can block ads you don't want. Exclusivity, content rules, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/service.mdx
new file mode 100644
index 00000000..740f199d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/service.mdx
@@ -0,0 +1,3 @@
+## What does PubFuture do for you?
+
+An ad operations team supports you, and PubFuture says it replies within 24 hours. It tunes your ads and sends reports.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx
index 7531ce64..a112ea47 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx
@@ -1 +1,19 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Does PubGalaxy take sites below its suggested traffic?
+
+It can. The traffic level is a suggestion, not a hard rule, and each site is reviewed on its own.
+
+## Why does PubGalaxy list two shares?
+
+One covers ads sold through its ad partners, and a lower one covers direct deals. Both are minimums in the published terms.
+
+## What does PubGalaxy check before it accepts you?
+
+Your traffic history, your content, and any copyright claims against your site in Google's Transparency Report.
+
+## Is PubGalaxy still running?
+
+Yes, as part of Improve Digital. Azerion bought PubGalaxy in 2021 and moved its publisher services under the Improve Digital name in September 2026. The pubgalaxy.com site now opens Improve Digital's site, so new publishers start there. Sites that already run PubGalaxy still name it in their ads.txt files, so we keep listing them here.
+
+## How can you check if a site uses PubGalaxy?
+
+Look for PubGalaxy in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubGalaxy.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx
index 99be0017..dd81b891 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx
@@ -1 +1 @@
-Ad revenue optimization for publishers. A site can use more than one ad network.
+PubGalaxy is an ad revenue service that reviews each publisher one by one. It suits sites with original, brand-safe content and some traffic history. It's now part of Improve Digital, which Azerion owns.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/joining.mdx
new file mode 100644
index 00000000..2f1bb724
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join PubGalaxy?
+
+PubGalaxy has no hard entry rules. It suggests a traffic level but reviews each site on its own. It asks for six months of traffic history and checks the Google Transparency Report for copyright claims against your site.
+
+Content should be original and brand safe. Stolen or automated content isn't allowed, and neither are violence, nudity, gambling, or banned substances. Apply through its publisher contact page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/pay.mdx
new file mode 100644
index 00000000..da1b561d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/pay.mdx
@@ -0,0 +1,5 @@
+## How does PubGalaxy pay?
+
+The published terms give you at least 85% of net revenue from ad partners and at least 65% from direct deals. PubGalaxy pays within 35 days after the month ends, once it receives the advertiser's money.
+
+The currency is not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/rules.mdx
new file mode 100644
index 00000000..4f8fdfba
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does PubGalaxy set?
+
+You must own or license your content and let PubGalaxy manage your ad inventory. It can make changes that improve the reader experience, and the terms set rules for direct deals.
+
+PubGalaxy says terms can vary by publisher. Check exclusivity and notice in the order you sign.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/service.mdx
new file mode 100644
index 00000000..d817d4ae
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/service.mdx
@@ -0,0 +1,3 @@
+## What does PubGalaxy do for you?
+
+You get detailed earnings reports and access to direct and private marketplace deals. Support response times are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx
index 7531ce64..839b8581 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Does Publift care more about revenue or traffic?
+
+Revenue. Publift uses monthly ad revenue as its main test and treats pageviews as a hint. A site with fewer pageviews but strong earnings can still fit.
+
+## When does Publift pay?
+
+It depends on where the money comes from. Google revenue is paid sooner than revenue from other networks. Check the dates in your order.
+
+## How can you check if a site uses Publift?
+
+Look for Publift in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Publift.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx
index 3619d0ae..1fdbf381 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx
@@ -1 +1 @@
-Publisher monetization platform using Fuse technology. A site can use more than one ad network.
+Publift is a managed ad service built on its Fuse platform. It qualifies sites by ad revenue first, so it suits sites that already earn steadily.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/joining.mdx
new file mode 100644
index 00000000..9ca68383
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Publift?
+
+Publift looks at your monthly ad revenue first. Pageviews are a secondary signal, not a strict rule. Your traffic should be steady and genuine, and your content useful and safe.
+
+You apply for a site review. Publift doesn't say which regions it accepts.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/pay.mdx
new file mode 100644
index 00000000..a3510725
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/pay.mdx
@@ -0,0 +1,5 @@
+## How does Publift pay?
+
+Publift's comparison page lists payment by direct bank transfer, with Google revenue on Net 30 and other network revenue on Net 90. That page is marketing content, so confirm the currency and terms in your order.
+
+The share is not on its requirements page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/rules.mdx
new file mode 100644
index 00000000..e9e22070
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Publift set?
+
+The requirements page doesn't state exclusivity, layout control, banned topics, contract length, or notice. A Publift blog post describes month-to-month service and a revenue split, but that's general guidance. Confirm both in your order.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/service.mdx
new file mode 100644
index 00000000..6ffa8a5f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/service.mdx
@@ -0,0 +1,3 @@
+## What does Publift do for you?
+
+The Fuse platform tunes your ads and reports on them. Publift says you get a dedicated account management team.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx
index 7531ce64..5a8b165e 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Which Publir model fits you?
+
+Pro suits a site that wants Publir to run its ads for a fee. Custom ad deals under the service agreement work on a revenue split instead. Ask which model you'd be on before you sign.
+
+## Can Publir change its fees?
+
+Yes. The service agreement allows Publir to change its percentages for costs and fees.
+
+## What else does Publir offer besides ads?
+
+Ad block recovery, subscriptions, crowdfunding, and QuickShop. These can add income beyond ad slots.
+
+## How can you check if a site uses Publir?
+
+Look for Publir in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Publir.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx
index 03592a7f..e586edae 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx
@@ -1 +1 @@
-Ad optimization and audience monetization platform for publishers. A site can use more than one ad network.
+Publir is an ad optimization and audience monetization platform. It has no traffic or revenue minimum, so sites of any size can try it.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/joining.mdx
new file mode 100644
index 00000000..ca23e7c7
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Publir?
+
+Publir has no traffic or revenue limit. It offers Pro and Enterprise plans, and you apply from its pricing page. Location and content rules are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/pay.mdx
new file mode 100644
index 00000000..1f658dc2
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/pay.mdx
@@ -0,0 +1,5 @@
+## How does Publir pay?
+
+Publir has two models. Its Pro service charges a 15% fee. Under its service agreement, custom ad revenue is split 50/50 and paid on Net 60. There's no single share for every publisher.
+
+The payment minimum, methods, and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/rules.mdx
new file mode 100644
index 00000000..9db87427
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Publir set?
+
+The service agreement lets Publir change its percentages to cover costs and fees. Exclusivity, content rules, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/service.mdx
new file mode 100644
index 00000000..f1f60424
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/service.mdx
@@ -0,0 +1,5 @@
+## What does Publir do for you?
+
+Publir offers ad optimization, ad block recovery, subscriptions, crowdfunding, and QuickShop. Pro support is by email during business hours. Enterprise adds real-time analytics and phone support.
+
+The service agreement says onboarding can take ten business days. Support runs weekdays from 8 a.m. to 5 p.m. Eastern, with a 12-hour reply target outside those hours.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx
index 7531ce64..17848d97 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx
@@ -1 +1,15 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## When does Publisher Collective pay you?
+
+After the advertiser pays Publisher Collective. The partner terms don't set a fixed interval, so ask how long that usually takes.
+
+## What do you give up with Publisher Collective?
+
+You can't edit its ad tags, and you may need to send advertiser leads its way. Read those terms closely if you also sell ads directly.
+
+## What happened to Snigel?
+
+Snigel and Publisher Collective merged in February 2025. Publisher Collective now sells under its own name and keeps Snigel as its ad technology. Many sites still name Snigel's domain, snigelweb.com, in their ads.txt files. We count those sites under Publisher Collective.
+
+## How can you check if a site uses Publisher Collective?
+
+Look for Publisher Collective or Snigel in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Publisher Collective.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx
index 7cf32bff..4a174ce6 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx
@@ -1 +1 @@
-Publisher monetization service and network. A site can use more than one ad network.
+Publisher Collective is a monetization service and network that sells ads for its publishers. Each publisher signs its own representation agreement. It merged with Snigel in 2025, so we count sites that name Snigel in their ads.txt files here too.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/joining.mdx
new file mode 100644
index 00000000..979560f7
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Publisher Collective?
+
+There's no published traffic minimum. You must own or have rights to your content and follow its terms. Apply through its publisher contact page, then sign an Advertising Representation Agreement.
+
+Regions aren't fixed in the public terms.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/pay.mdx
new file mode 100644
index 00000000..8e205e1d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/pay.mdx
@@ -0,0 +1,5 @@
+## How does Publisher Collective pay?
+
+Publisher Collective pays after the advertiser pays it, by bank transfer in GBP, EUR, or USD. Your share is set in your representation agreement.
+
+A Publisher Collective blog post lists a different share, timing, and minimum. Those conflict with the partner terms, so go by the terms you sign.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/rules.mdx
new file mode 100644
index 00000000..df187b68
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Publisher Collective set?
+
+Discriminatory, weapons, porn, hateful, and indecent content isn't allowed. You may need to update site materials within seven days, you can't change the ad tags, and you may need to pass ad leads to Publisher Collective.
+
+It can suspend or end the deal if you don't respond. Exclusivity and contract length are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/service.mdx
new file mode 100644
index 00000000..2b8f98cf
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/service.mdx
@@ -0,0 +1,3 @@
+## What does Publisher Collective do for you?
+
+Not public. The FAQ points you to apply and sign your own terms, but doesn't describe a dashboard, reports, or support.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx
index 7531ce64..877ede9b 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## How do you join PubNation?
+
+PubNation doesn't publish how to apply. Contact the company through its website.
+
+## What should you ask PubNation before you sign?
+
+Ask for the share, when you'd be paid, the payment minimum, and whether it must be your only ad partner. Ask how much notice you'd need to leave.
+
+## How can you check if a site uses PubNation?
+
+Look for PubNation in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubNation.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx
index 8277d893..c4bda632 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx
@@ -1 +1 @@
-Publisher monetization company. A site can use more than one ad network.
+PubNation is a publisher monetization company. Its rules and terms aren't published on its website.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/joining.mdx
new file mode 100644
index 00000000..4f8f096e
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join PubNation?
+
+Not public. PubNation doesn't publish a traffic minimum, site rules, accepted regions, or how to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/pay.mdx
new file mode 100644
index 00000000..cc8523db
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/pay.mdx
@@ -0,0 +1,3 @@
+## How does PubNation pay?
+
+Not public. The share, payment timing, minimum, methods, and currency aren't on its website.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/rules.mdx
new file mode 100644
index 00000000..b0506a5f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does PubNation set?
+
+Not public. Exclusivity, ad control, content rules, contract length, and notice aren't on its website.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/service.mdx
new file mode 100644
index 00000000..c33e8ef1
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/service.mdx
@@ -0,0 +1,3 @@
+## What does PubNation do for you?
+
+Not public. PubNation doesn't describe its reports, dashboard, support, or extra tools.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx
index 7531ce64..81d3221a 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Why do some PubPower sites get paid sooner?
+
+PubPower pays sites above its monthly revenue line as soon as the month ends, with no waiting period. Other sites wait for the standard term.
+
+## Which PubPower payment method should you pick?
+
+Pick one with a minimum your earnings clear. Payoneer has the lowest minimum, and bank transfer the highest.
+
+## How can you check if a site uses PubPower?
+
+Look for PubPower in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubPower.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx
index bd0dac31..64c12a2a 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx
@@ -1 +1 @@
-Publisher ad management platform. A site can use more than one ad network.
+PubPower is an ad management platform for publishers. Sites that earn more get paid faster.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/joining.mdx
new file mode 100644
index 00000000..9495dcc1
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join PubPower?
+
+Not public. PubPower doesn't publish a traffic minimum, content rules, accepted regions, or how to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/pay.mdx
new file mode 100644
index 00000000..81f92e0b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/pay.mdx
@@ -0,0 +1,5 @@
+## How does PubPower pay?
+
+If you earn more than $3,000 in a month, PubPower pays on Net 0. Otherwise it pays on Net 30.
+
+The minimum depends on the method: $50 for Payoneer, $100 for PayPal, $100 to $1,000 for crypto, and $1,000 for a bank transfer. The share and payout currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/rules.mdx
new file mode 100644
index 00000000..909a51e4
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does PubPower set?
+
+Not public. Exclusivity, placement control, content rules, contract length, and notice aren't on its payment page.
+
+A revenue share article on its help site is for ad networks using its white-label product, not for publishers.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/service.mdx
new file mode 100644
index 00000000..339cb1f8
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/service.mdx
@@ -0,0 +1,3 @@
+## What does PubPower do for you?
+
+Not public. PubPower doesn't describe its publisher dashboard, support, or reports on the page we checked.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx
index 7531ce64..de28bd94 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is Raptive a fit for your site?
+
+It is if you write original content and most of your readers are in the US, Canada, UK, Australia, or New Zealand. Sites with most traffic elsewhere won't qualify.
+
+## Does your Raptive service change as you grow?
+
+Yes. A dedicated manager, custom layouts, and fuller reporting depend on your tier.
+
+## How can you check if a site uses Raptive?
+
+Look for Raptive in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Raptive.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx
index 82a62ed1..9c624913 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx
@@ -1 +1 @@
-Ad management and monetization for creators. A site can use more than one ad network.
+Raptive is a managed ad service for creators and independent publishers. It suits established sites with most readers in the US, Canada, UK, Australia, or New Zealand.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/joining.mdx
new file mode 100644
index 00000000..a7dbf63c
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Raptive?
+
+Most of your traffic must come from the US, Canada, UK, Australia, or New Zealand. You need original content and Google Analytics set up.
+
+Apply through Raptive's creator application.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/pay.mdx
new file mode 100644
index 00000000..6bb39bba
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/pay.mdx
@@ -0,0 +1,3 @@
+## How does Raptive pay?
+
+Raptive pays on the 15th of the month, on Net 45 terms. The minimum is higher if you choose a wire transfer.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/rules.mdx
new file mode 100644
index 00000000..65703cf1
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Raptive set?
+
+Raptive sets up standard or custom ad layouts, depending on your size. Check placement controls in the agreement you sign. Exclusivity, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/service.mdx
new file mode 100644
index 00000000..4b9217ad
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/service.mdx
@@ -0,0 +1,3 @@
+## What does Raptive do for you?
+
+You get a dashboard and support tickets. Depending on your tier, you also get a dedicated manager, and reporting and layout options differ by tier.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx
index 7531ce64..d4bfc998 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## When does Refinery89 pay?
+
+It depends on your terms. Refinery89 has more than one payment schedule, so ask your Publisher Success Manager which one is yours.
+
+## What can you see in the Refinery89 dashboard?
+
+Daily revenue, CPM, RPM, pageviews, fill rate, and viewability, plus invoices and support tickets. You can export your data and manage ads.txt there too.
+
+## How can you check if a site uses Refinery89?
+
+Look for Refinery89 in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Refinery89.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx
index 1bb3f8d1..6f029520 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx
@@ -1 +1 @@
-Publisher ad monetization service. A site can use more than one ad network.
+Refinery89 is an ad monetization service for publishers. Each publisher works with a Publisher Success Manager.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/joining.mdx
new file mode 100644
index 00000000..4c32720d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Refinery89?
+
+Not public. Refinery89 doesn't publish a traffic minimum, site rules, eligible regions, or how to apply. Contact its publisher team.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/pay.mdx
new file mode 100644
index 00000000..542691d3
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/pay.mdx
@@ -0,0 +1,5 @@
+## How does Refinery89 pay?
+
+The payment minimum depends on your region. Refinery89 publishes more than one set of payment terms, so ask your Publisher Success Manager for your schedule.
+
+The share, methods, and currency are not fully public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/rules.mdx
new file mode 100644
index 00000000..50925783
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Refinery89 set?
+
+Not public. Exclusivity, placement control, content rules, contract length, and notice aren't on its publisher pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/service.mdx
new file mode 100644
index 00000000..6a6388e9
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/service.mdx
@@ -0,0 +1,5 @@
+## What does Refinery89 do for you?
+
+The dashboard shows daily revenue, CPM, RPM, pageviews, fill rate, and viewability. It also has invoice history, tickets, exports, and ads.txt management.
+
+You can reach your success manager through a Chrome plugin, the dashboard, WhatsApp, email, or phone.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx
index 7531ce64..15782d57 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## How is your Revcontent share set?
+
+You negotiate it. Revcontent doesn't publish a standard rate, so compare its offer with what you earn now.
+
+## What's the payment minimum at Revcontent?
+
+It depends. The terms set one floor, and each payment method sets its own. Check your dashboard or order for the one that applies to you.
+
+## How can you check if a site uses Revcontent?
+
+Look for Revcontent in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Revcontent.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx
index 98750459..fc370abf 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx
@@ -1 +1 @@
-Content recommendation and native advertising platform for publishers. A site can use more than one ad network.
+Revcontent is a content recommendation and native advertising platform for publishers. Its share is negotiated with each publisher.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/joining.mdx
new file mode 100644
index 00000000..f6e0c9b3
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Revcontent?
+
+Revcontent doesn't publish a traffic minimum or region rule. You apply for approval, and your site must follow its publisher terms and content rules.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/pay.mdx
new file mode 100644
index 00000000..3e785b9c
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/pay.mdx
@@ -0,0 +1,5 @@
+## How does Revcontent pay?
+
+Your share is negotiated. The terms work out your revenue after allowed fees and set a payment floor.
+
+Each method also has its own minimum: $100 for ACH in the US, $100 for PayPal outside the US, and $1,000 for international wire. These don't add up to one clear threshold, so check your dashboard or order. Payment is in US dollars.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/rules.mdx
new file mode 100644
index 00000000..aca1dc46
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Revcontent set?
+
+The publisher terms set policy and placement rules. They don't state one exclusivity rule or contract term for everyone. Content rules are in the publisher policy.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/service.mdx
new file mode 100644
index 00000000..9950a7f3
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/service.mdx
@@ -0,0 +1,3 @@
+## What does Revcontent do for you?
+
+Not public. Revcontent's terms don't describe support, reporting, or dashboard service levels.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx
index 7531ce64..406d9fb2 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Why are Setupad's rules unclear?
+
+Setupad's published rules are in blog articles from different years, and they don't fully agree. Ask its team for current eligibility before you count on them.
+
+## How can Setupad pay you?
+
+By bank transfer, PayPal, Payoneer, Wise, Revolut, or Paysera, according to its guidance.
+
+## How can you check if a site uses Setupad?
+
+Look for Setupad in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Setupad.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx
index 81882588..a7649b7f 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx
@@ -1 +1 @@
-Publisher platform for display ad monetization. A site can use more than one ad network.
+Setupad is a platform that runs display ads for publishers. Its published rules come from blog articles, so check current terms with its team.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/joining.mdx
new file mode 100644
index 00000000..20436579
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join Setupad?
+
+Setupad's articles from different years describe a traffic rule, but one counts visitors and another counts sessions. They also mention traffic from the US, UK, Canada, and Australia. We didn't find a current criteria page, so confirm with Setupad.
+
+You need to follow ad policies and set up ads.txt. Apply through Setupad's publisher contact page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/pay.mdx
new file mode 100644
index 00000000..e6e1df99
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/pay.mdx
@@ -0,0 +1,3 @@
+## How does Setupad pay?
+
+Setupad's published guidance lists Net 60 payment. The share and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/rules.mdx
new file mode 100644
index 00000000..bb1e5f69
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Setupad set?
+
+You need to follow ad policies and keep an ads.txt file. Exclusivity, ad placement control, contract length, and notice are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/service.mdx
new file mode 100644
index 00000000..0de1377d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/service.mdx
@@ -0,0 +1,3 @@
+## What does Setupad do for you?
+
+Not public. Setupad's articles don't describe its dashboard, reports, or support levels.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx
index 7531ce64..e62919ed 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## How do you join SHE Media?
+
+SHE Media doesn't publish a general application checklist. Its referral program lists criteria for referred sites, which is the clearest public guide.
+
+## Can publishers outside the US work with SHE Media?
+
+Yes. SHE Media has payment options for publishers outside the US, including PayPal, Wise, ACH, and wire. Payoneer isn't one of them.
+
+## How can you check if a site uses SHE Media?
+
+Look for SHE Media in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to SHE Media.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx
index 4e89fd3d..baf495ba 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx
@@ -1 +1 @@
-Publisher partner network run by SHE Media. A site can use more than one ad network.
+SHE Media runs a partner network for publishers. Its joining rules and pay share are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/joining.mdx
new file mode 100644
index 00000000..1a1154bd
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/joining.mdx
@@ -0,0 +1,5 @@
+## Who can join SHE Media?
+
+SHE Media doesn't publish a general traffic minimum. Its referral program mentions a traffic level for referred sites, along with quality content and Google Analytics. That's for referrals, not every applicant.
+
+Regions and a current checklist are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/pay.mdx
new file mode 100644
index 00000000..c2e88161
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/pay.mdx
@@ -0,0 +1,5 @@
+## How does SHE Media pay?
+
+The share, payment schedule, and general minimum are not public.
+
+Outside the US, SHE Media pays by PayPal if you earn under $1,000 a month, and prefers wire transfer above that. Wise and ACH are options too. Payoneer isn't accepted. Currency is not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/rules.mdx
new file mode 100644
index 00000000..f9b16605
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does SHE Media set?
+
+Not public. Exclusivity, ad placement control, content rules, contract length, and notice aren't on its publisher help pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/service.mdx
new file mode 100644
index 00000000..d93d8473
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/service.mdx
@@ -0,0 +1,3 @@
+## What does SHE Media do for you?
+
+The Publisher Portal shows reports, at least for referral payouts. General dashboard reports, support times, and tools are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx
index 7531ce64..f85a9959 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Can you leave Snack Media?
+
+Yes. Either side can end the contract with notice, and after the first term it runs month to month.
+
+## Does Snack Media need to be your only ad partner?
+
+Only if your order says so. Check the cover sheet before you sign.
+
+## How can you check if a site uses Snack Media?
+
+Look for Snack Media in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Snack Media.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx
index e585d510..16371e41 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx
@@ -1 +1 @@
-Publisher advertising and monetization services. A site can use more than one ad network.
+Snack Media is a publisher ad network and monetization service. After the first contract term, it runs month to month.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/joining.mdx
new file mode 100644
index 00000000..b099580d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Snack Media?
+
+Not public. Snack Media doesn't publish a traffic minimum, content rules, regions, or an application process. Contact it through its site.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/pay.mdx
new file mode 100644
index 00000000..5d39abcc
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/pay.mdx
@@ -0,0 +1,5 @@
+## How does Snack Media pay?
+
+Snack Media pays within 30 days after the month it receives the advertiser's money. Your share is set on the cover sheet you sign. The standard terms don't give one rate, minimum, method, or currency.
+
+Older Snack Media terms list a different minimum and timing. We go by the 2025 agreement instead.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/rules.mdx
new file mode 100644
index 00000000..ca69d78a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Snack Media set?
+
+Snack Media is exclusive only if your order says so. The cover sheet sets the first contract term, and its length is not public. After that, the contract renews month to month.
+
+Either side can end the contract with 30 days' notice.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/service.mdx
new file mode 100644
index 00000000..3decafe8
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/service.mdx
@@ -0,0 +1,3 @@
+## What does Snack Media do for you?
+
+Not public. The standard terms don't describe a dashboard, reports, support, or extra tools.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snigel/article.mdx
deleted file mode 100644
index 7531ce64..00000000
--- a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/article.mdx
+++ /dev/null
@@ -1 +0,0 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snigel/intro.mdx
deleted file mode 100644
index 4f5a1524..00000000
--- a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/intro.mdx
+++ /dev/null
@@ -1 +0,0 @@
-Publisher ad management brand. A site can use more than one ad network.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx
index 7531ce64..0245d4ec 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## What's the difference between Sovrn's Ad Exchange and Sovrn Commerce?
+
+The Ad Exchange sells ad space on your site. Sovrn Commerce is a separate affiliate product with its own payment terms. The details on this page are for the Ad Exchange.
+
+## Is Sovrn a fit for a small site?
+
+It can be. The Ad Exchange doesn't set a traffic minimum, and its payment minimum is low.
+
+## How can you check if a site uses Sovrn?
+
+Look for Sovrn in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Sovrn.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx
index 6e2eb8d3..5498d669 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx
@@ -1 +1 @@
-Advertising and monetization products for publishers. A site can use more than one ad network.
+Sovrn offers an ad exchange and other monetization tools for publishers. Its Ad Exchange has no published traffic minimum.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/joining.mdx
new file mode 100644
index 00000000..3285008a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Sovrn?
+
+You need a working business domain and an ad system ID, and you must keep your ads.txt file up to date. Sovrn's approval guide doesn't set a traffic minimum.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/pay.mdx
new file mode 100644
index 00000000..ff6505d2
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/pay.mdx
@@ -0,0 +1,5 @@
+## How does Sovrn pay?
+
+The Ad Exchange pays on Net 45, in US dollars, with some options to receive local currency.
+
+Sovrn Commerce, its separate affiliate product, has different payment terms.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/rules.mdx
new file mode 100644
index 00000000..cbfadc0b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Sovrn set?
+
+The Ad Exchange approval and payment pages don't state exclusivity, placement control, content rules, contract length, or notice. Sovrn Commerce has its own terms.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/service.mdx
new file mode 100644
index 00000000..de13a37f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/service.mdx
@@ -0,0 +1,3 @@
+## What does Sovrn do for you?
+
+Sovrn's knowledge base covers account upkeep and payments. Dashboard reports and support levels are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx
index 7531ce64..a9b9931d 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## What does Sparteo offer besides display ads?
+
+Video through Viously, consent management through FastCMP, and Actirise, plus analytics on revenue and page speed.
+
+## How do you work with Sparteo?
+
+Sparteo doesn't publish how to apply. Contact the company to ask.
+
+## How can you check if a site uses Sparteo?
+
+Look for Sparteo in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Sparteo.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx
index 118d5d4f..de1aee28 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx
@@ -1 +1 @@
-Publisher monetization technology and managed advertising services. A site can use more than one ad network.
+Sparteo is an ad technology company with managed ad services for publishers. Its services include Actirise, Viously video, and FastCMP.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/joining.mdx
new file mode 100644
index 00000000..0760291b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Sparteo?
+
+Not public. Sparteo doesn't publish a traffic minimum, site rules, regions, or how to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/pay.mdx
new file mode 100644
index 00000000..9d7d8d42
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/pay.mdx
@@ -0,0 +1,3 @@
+## How does Sparteo pay?
+
+Not public. Sparteo says it earns money when its publishers do, but it doesn't publish a share, payment timing, minimum, methods, or currency.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/rules.mdx
new file mode 100644
index 00000000..0ce83338
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Sparteo set?
+
+Not public. Exclusivity, placement control, content rules, contract length, and notice aren't on its public pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/service.mdx
new file mode 100644
index 00000000..e83cdefa
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/service.mdx
@@ -0,0 +1,5 @@
+## What does Sparteo do for you?
+
+Sparteo covers display ads, analytics, Actirise, Viously video, and FastCMP, a consent management tool. Its reports show revenue, viewability, time, URL, and Core Web Vitals.
+
+Support terms are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx
index 7531ce64..064cf4ad 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Can you run Taboola with another recommendation widget?
+
+No. Taboola must be your only recommendation service during the agreement. The rule covers recommendation services, so check your order for anything else.
+
+## What content can't run Taboola?
+
+Fake news, porn, copyright violations, defamation, threats, and criminal material are banned under its Publisher Policy.
+
+## How can you check if a site uses Taboola?
+
+Look for Taboola in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Taboola.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx
index 4fdcf874..4643d699 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx
@@ -1 +1 @@
-Content recommendation and advertising platform for publishers. A site can use more than one ad network.
+Taboola is a content recommendation and advertising platform for publishers. It shows recommended stories and ads on your pages.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/joining.mdx
new file mode 100644
index 00000000..dbed98c7
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Taboola?
+
+Taboola doesn't publish a traffic minimum or region rule. Your site must follow the Taboola Publisher Policy. Apply by contacting Taboola.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/pay.mdx
new file mode 100644
index 00000000..c3f48c6c
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/pay.mdx
@@ -0,0 +1,3 @@
+## How does Taboola pay?
+
+Your share is set in your order. Taboola pays in US dollars within 45 days after the month ends, through Payoneer. A general payment minimum is not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/rules.mdx
new file mode 100644
index 00000000..33f82f3d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/rules.mdx
@@ -0,0 +1,5 @@
+## What rules does Taboola set?
+
+While your agreement runs, Taboola must be your only recommendation service. The policy bans fake news, porn, copyright violations, defamation, threats, and criminal content.
+
+Your order sets the contract length and how to end it.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/service.mdx
new file mode 100644
index 00000000..45cdea36
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/service.mdx
@@ -0,0 +1,3 @@
+## What does Taboola do for you?
+
+Taboola offers publisher support resources and policy help. Dashboard reports and dedicated support levels are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx
index 7531ce64..09b8a416 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Is Teads the same as Outbrain?
+
+Teads now includes what used to be Outbrain's business.
+
+## How do you join Teads?
+
+Teads doesn't publish an application process. Its media owner page is where to start a partnership conversation.
+
+## How can you check if a site uses Teads?
+
+Look for Teads in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Teads.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx
index 049c4ddc..86e85736 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx
@@ -1 +1 @@
-Publisher advertising platform that includes the former Outbrain business. A site can use more than one ad network.
+Teads is a publisher advertising platform for web, connected TV, and apps. It now includes the former Outbrain business.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/joining.mdx
new file mode 100644
index 00000000..9631c698
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Teads?
+
+Not public. Teads describes partnerships across web, CTV, and apps, but doesn't publish a traffic minimum, content rules, regions, or how to apply.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/pay.mdx
new file mode 100644
index 00000000..87273b6c
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/pay.mdx
@@ -0,0 +1,5 @@
+## How does Teads pay?
+
+Your share is set in your contract. A Teads company filing says publisher payment terms usually run 30 to 60 days, but that's not one fixed term.
+
+The payment minimum, methods, and currency are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/rules.mdx
new file mode 100644
index 00000000..4c1e146a
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Teads set?
+
+Teads describes editorial controls and lets you control which ads are blocked. Exclusivity, content rules, contract length, and notice are not public, so confirm them in your contract.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/service.mdx
new file mode 100644
index 00000000..38576db6
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/service.mdx
@@ -0,0 +1,3 @@
+## What does Teads do for you?
+
+Teads offers premium ad placements across web, CTV, and apps, plus content extension. Its media owner page mentions partnership support, but not dashboard or reporting levels.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx
index 7531ce64..e3366bfc 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## When does The Moneytizer pay you?
+
+A couple of months after the month you earn it. The payment simulator on its site shows how the timing works.
+
+## How do you join The Moneytizer?
+
+Sign up on its site. The Moneytizer doesn't publish entry rules, so its team decides.
+
+## How can you check if a site uses The Moneytizer?
+
+Look for The Moneytizer in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to The Moneytizer.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx
index a20fbb64..3589454e 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx
@@ -1 +1 @@
-Managed website monetization and header bidding for publishers. A site can use more than one ad network.
+The Moneytizer is a managed monetization and header bidding service for publishers. Its payment minimum is low.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/joining.mdx
new file mode 100644
index 00000000..78382207
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join The Moneytizer?
+
+Not public. The Moneytizer doesn't publish a traffic minimum, content rules, regions, or application steps. Its site has a publisher sign-up page.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/pay.mdx
new file mode 100644
index 00000000..cc37f93c
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/pay.mdx
@@ -0,0 +1,3 @@
+## How does The Moneytizer pay?
+
+After each month ends, The Moneytizer creates your invoice, and payment follows about two months later. The payment method and share are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/rules.mdx
new file mode 100644
index 00000000..7f436511
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does The Moneytizer set?
+
+Not public. Exclusivity, placement control, content rules, contract length, and notice aren't in its payment information.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/service.mdx
new file mode 100644
index 00000000..5f918eb0
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/service.mdx
@@ -0,0 +1,3 @@
+## What does The Moneytizer do for you?
+
+Not public. Its site doesn't describe a dashboard, reports, support, or extra tools in enough detail to list here.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx
index 7531ce64..fa4301a4 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx
@@ -1 +1,11 @@
-A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use.
+## Who is Venatus for?
+
+Publishers in gaming, sports, and entertainment. If your site covers those topics, Venatus is built around that kind of audience.
+
+## How do you apply to Venatus?
+
+Fill in the publisher interest form on its site. Venatus doesn't publish entry rules, so its team decides.
+
+## How can you check if a site uses Venatus?
+
+Look for Venatus in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Venatus.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx
index 744f98d2..996c4af3 100644
--- a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx
@@ -1 +1 @@
-Advertising and monetization for publisher partners. A site can use more than one ad network.
+Venatus is an ad partner for gaming, sports, and entertainment sites.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/joining.mdx
new file mode 100644
index 00000000..e68ece34
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/joining.mdx
@@ -0,0 +1,3 @@
+## Who can join Venatus?
+
+Venatus works with gaming, sports, and entertainment sites. It doesn't publish a traffic minimum, region rule, or content checklist. You can send its publisher interest form.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/pay.mdx
new file mode 100644
index 00000000..42ff6a9d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/pay.mdx
@@ -0,0 +1,3 @@
+## How does Venatus pay?
+
+Not public. Venatus doesn't publish its share, payment timing, minimum, methods, or currency.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/rules.mdx
new file mode 100644
index 00000000..1dcab4fd
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/rules.mdx
@@ -0,0 +1,3 @@
+## What rules does Venatus set?
+
+Not public. Venatus describes curated demand and ad management, but not exclusivity, placement control, content rules, contract length, or notice.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/service.mdx
new file mode 100644
index 00000000..63fe1469
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/service.mdx
@@ -0,0 +1,3 @@
+## What does Venatus do for you?
+
+Venatus says expert publisher teams manage your ads and bring curated demand. Dashboard and reporting details are not public.
diff --git a/app/(site)/(directories)/ad-networks/[slug]/network-details.tsx b/app/(site)/(directories)/ad-networks/[slug]/network-details.tsx
new file mode 100644
index 00000000..326c04c7
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/[slug]/network-details.tsx
@@ -0,0 +1,109 @@
+import type { MDXContent } from "mdx/types";
+import type { ReactNode } from "react";
+import { DetailsLayout, FactsList, KeyNumbers, NotPublic, SourcesLine, type Fact, type KeyNumber } from "@/app/ui/ad-networks/details";
+import { formatDay } from "@/app/ui/ad-networks/site-format";
+import { proseClassName, proseFlowClassName } from "@/app/ui/site/prose";
+import { UiTabs, type UiTab } from "@/app/ui/tabs";
+import { NOT_PUBLIC, joinPhrase, joiningNumbers, networkFacts } from "../ad-networks-shared";
+import type { AdNetworkFacts } from "../network-facts";
+import type { AdNetworkContent } from "./content";
+import styles from "./network.module.css";
+
+type KeyFact = AdNetworkFacts["keyNumbers"][number];
+
+// The mockup's words for the labels research uses.
+const keyLabels: Record = { "your share": "revenue share" };
+
+/**
+ * One key number in the mockup's form: the value, then a short label. A joining rule reads as
+ * `sessions a month to join`; anything after the first `;` in its note stays as a caveat.
+ */
+function keyNumber(item: KeyFact, joining: boolean): KeyNumber {
+ const [first = "", ...rest] = (item.note ?? "").split(";");
+ if (joining) {
+ const phrase = joinPhrase(item);
+ if (phrase.startsWith(item.value) && phrase.length > item.value.length) {
+ // joinPhrase keeps only the unit; whatever it drops ("or 2 million …", ", for new sites") stays as the caveat.
+ const alternative = first.match(/\s(or\s+[^,]*\d.*)$/i)?.[1];
+ const clause = first.replace(/\s+or\s+(?=[^,]*\d).*$/i, "").split(",").slice(1).join(",").trim();
+ const caveat = [alternative, clause, rest.join(";").trim()].filter(Boolean).join("; ");
+ return { value: item.value, label: `${phrase.slice(item.value.length).trim()} to join`, note: caveat || undefined };
+ }
+ }
+ const label = item.label.charAt(0).toLowerCase() + item.label.slice(1);
+ return { value: item.value, label: keyLabels[label] ?? label, note: [first, ...rest].join(";").trim() || undefined };
+}
+
+/** Tab files start at `##`. They sit under the h3 panel heading UiTabs adds, so they render one level down. */
+const tabComponents = {
+ h2: ({ children }: { children?: ReactNode }) =>
,
+};
+
+/** Article questions sit under the details h2, so their `##` headings render as h3. */
+const articleComponents = {
+ h2: ({ children }: { children?: ReactNode }) =>
+
+ {rule ? <> {joinSentence(rule)}> : null}
+ {/* data-gate: the sync fills the listed and confirmed counts. */}
+ {network.listed_count ? (
+ network.listed_count === 1
+ ? <> We list {1} site that uses {network.name}, and it {network.verified_count ? "is" : "isn't"} confirmed live from its own ads.txt file or ad code.>
+ : <> We list {network.listed_count} sites that use {network.name}, and {network.verified_count} {network.verified_count === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code.>
+ ) : null}
+
{site.relationship_type === "primary_or_exclusive_manager" ? "Primary or exclusive monetization manager" : "Uses this network"}
-
)}
;
-}
-
-// The static page shows the first 30 sites. ?q= and ?page= use the full list in the browser.
-export function PublisherList({ firstPage, index, networkName, route, total }: {
- firstPage: VerifiedPublisher[];
- index: DirectoryIndexSource;
- networkName: string;
- route: string;
- total: number;
-}) {
- const { params } = useDirectoryQuery();
- const filtering = hasDirectoryParams(params, ["q", "page"]);
- const { failed, records } = useDirectoryIndex(index, filtering);
- const query = readQuery(params);
- const needle = query.toLowerCase();
- const matches = filtering && records ? records.filter((site) => site.normalized_domain.toLowerCase().includes(needle)) : null;
- const count = matches ? matches.length : total;
- const totalPages = Math.max(1, Math.ceil(count / publisherPageSize));
- const currentPage = Math.max(1, Math.min(readPage(params), totalPages));
- const rows = matches ? matches.slice((currentPage - 1) * publisherPageSize, currentPage * publisherPageSize) : firstPage;
- const hrefForPage = (page: number) => pageParamHref(route, query ? { q: query } : {}, page);
-
- let body;
- if (filtering && !records) {
- body = failed
- ? Show all verified sites} state="error" title="Sites could not be loaded">
Reload the page to try again.
- : ;
- } else if (rows.length) {
- body = <>
-
-
- >;
- } else {
- body = Show all verified sites : undefined} title={query ? "No verified sites match that search" : "No publisher sites verified yet"}>
{query ? "Try another part of the domain." : "This network will show sites here after their current records confirm a relationship."}
- {body}
- >;
-}
diff --git a/app/(site)/(directories)/ad-networks/ad-networks-shared.tsx b/app/(site)/(directories)/ad-networks/ad-networks-shared.tsx
new file mode 100644
index 00000000..52d09961
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/ad-networks-shared.tsx
@@ -0,0 +1,46 @@
+import type { SectionNav } from "@/app/ui/site/section-bar";
+import { formatCount } from "@/app/ui/ad-networks/site-format";
+import styles from "./ad-networks.module.css";
+
+// Words and helpers shared by the ad network pages. Numbers come from the database or
+// network-facts.ts; the MDX words never repeat them.
+
+export function adNetworksSectionNav(current: string): SectionNav {
+ return {
+ label: "ad networks",
+ href: "/ad-networks",
+ current,
+ items: [
+ { label: "Directory", href: "/ad-networks" },
+ { label: "Niches", href: "/ad-networks/niches" },
+ { label: "Stats", href: "/ad-networks/stats" },
+ { label: "Checker", href: "/ad-network-checker" },
+ ],
+ };
+}
+
+export { NOT_PUBLIC, joinLabel, joinOrder, joinPhrase, joinRule, joinSentence, joiningNumbers, networkFacts, trafficRule } from "./join-rules";
+
+/** A count in the header lede: mono, and accent-colored for confirmed sites. */
+export function Figure({ children, live = false }: { children: number; live?: boolean }) {
+ return {formatCount(children)};
+}
+
+/** The sentence after each intro that says how many sites we list and confirm. */
+export function CountsLine({ confirmed, listed, noun = "sites" }: { confirmed: number; listed: number; noun?: string }) {
+ if (!listed) return
We haven't confirmed any {noun} yet. They show up here once we confirm them from their own ads.txt files or ad code.
;
+ return (
+
+ We list {listed} {listed === 1 ? noun.replace(/\bsites\b/, "site") : noun}, and {confirmed} {confirmed === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code.
+
+ );
+}
+
+export function ordinal(value: number) {
+ const tens = value % 100;
+ if (tens >= 11 && tens <= 13) return `${value}th`;
+ return `${value}${{ 1: "st", 2: "nd", 3: "rd" }[value % 10] ?? "th"}`;
+}
+
+/** Whole percents, except near the ends: never show 0% or 100% for a value that isn't. */
+export { formatPercent as percent } from "@/app/ui/ad-networks/viz/format";
diff --git a/app/(site)/(directories)/ad-networks/ad-networks-sidebar.tsx b/app/(site)/(directories)/ad-networks/ad-networks-sidebar.tsx
deleted file mode 100644
index 2452ded3..00000000
--- a/app/(site)/(directories)/ad-networks/ad-networks-sidebar.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { SidebarLabel } from "@/app/ui/site/app-shell";
-import { SidebarList } from "@/app/ui/site/sidebar-list";
-import { sidebarPanelClassName, sidebarPanelHeadingClassName } from "@/app/ui/site/sidebar-panel";
-import type { AdNetwork } from "@/lib/ad-networks";
-
-export function AdNetworksSidebar({ networks, current }: { networks: AdNetwork[]; current?: string }) {
- return
-
Ad networks
-
-
;
-}
diff --git a/app/(site)/(directories)/ad-networks/ad-networks.module.css b/app/(site)/(directories)/ad-networks/ad-networks.module.css
new file mode 100644
index 00000000..eba218af
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/ad-networks.module.css
@@ -0,0 +1,9 @@
+/* A count in a header lede (mockup `.fig`): mono, and accent-colored for confirmed sites. */
+
+.figure {
+ font: var(--weight-semibold) 0.95em var(--font-mono);
+}
+
+.figure[data-live] {
+ color: var(--color-accent-text);
+}
diff --git a/app/(site)/(directories)/ad-networks/content/article.mdx b/app/(site)/(directories)/ad-networks/content/article.mdx
index 8b137891..8fee905b 100644
--- a/app/(site)/(directories)/ad-networks/content/article.mdx
+++ b/app/(site)/(directories)/ad-networks/content/article.mdx
@@ -1 +1,13 @@
+## What do ad networks require to join?
+Most ad networks set a minimum to join, shown on each card under "To join". Many count monthly traffic, and some, such as Mediavine, count the ad revenue your site already earns. Self-serve networks such as Google AdSense take sites of any size. Where a card says "Not public", the network doesn't publish a fixed rule.
+
+Networks count traffic in different ways. A session is one visit, and a pageview is one page loaded, so one session often includes several pageviews. A minimum of 50,000 sessions is harder to reach than 50,000 pageviews.
+
+Traffic isn't the only rule. Many networks also check where your visitors come from, whether your content is original, and whether your site follows Google's ad policies. Each network's page lists its full rules.
+
+## How do we confirm a site uses a network?
+
+We read each site's own ads.txt file, the public list of companies allowed to sell its ad space. When the file names a network as its ad manager, or the site's pages load that network's ad code, the site counts as confirmed. A network's seller list can point us to a site, but it never confirms one on its own.
+
+We re-check every site regularly, and a site that drops a network loses its confirmed status. That's why a network's confirmed count can be lower than its total.
diff --git a/app/(site)/(directories)/ad-networks/content/intro.mdx b/app/(site)/(directories)/ad-networks/content/intro.mdx
deleted file mode 100644
index 56c56744..00000000
--- a/app/(site)/(directories)/ad-networks/content/intro.mdx
+++ /dev/null
@@ -1 +0,0 @@
-Explore ad networks and the websites verified for each one. Counts include confirmed relationships only.
diff --git a/app/(site)/(directories)/ad-networks/directory-browser.tsx b/app/(site)/(directories)/ad-networks/directory-browser.tsx
new file mode 100644
index 00000000..6ae5dc9f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/directory-browser.tsx
@@ -0,0 +1,252 @@
+"use client";
+
+import Link from "next/link";
+import { Search } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from "react";
+import { UiButton } from "@/app/ui/controls";
+import { StatePanel } from "@/app/ui/state-panel";
+import { TableToolbar, ToolbarSelect } from "@/app/ui/table-toolbar";
+import { NetworkGrid, SortNote } from "@/app/ui/ad-networks/network-card";
+import { SectionHead } from "@/app/ui/ad-networks/section";
+import { checkerHref } from "@/app/ui/ad-networks/site-format";
+import type { AdNetworkDirectorySort } from "@/lib/ad-networks-types";
+import styles from "./directory.module.css";
+
+export type DirectoryNetwork = { slug: string; name: string };
+export type DirectoryNiche = { slug: string; name: string; /** Network slugs, most sites in this niche first. */ order: readonly string[] };
+
+type View = { q: string; sort: AdNetworkDirectorySort; niche: string };
+
+const ViewContext = createContext<{ view: View; set: (patch: Partial) => void } | null>(null);
+
+const visibleAtFirst = 12;
+const domainLike = /^(?:https?:\/\/)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[/?#].*)?$/i;
+const sortLabels: Record = {
+ "most-sites": "Most sites",
+ "easiest-to-join": "Easiest to join",
+ "by-niche": "By niche",
+};
+
+const cleanQuery = (value: string) => value.trim().slice(0, 100);
+const matches = (network: DirectoryNetwork, needle: string) =>
+ network.name.toLowerCase().includes(needle) || network.slug.includes(needle.replace(/\s+/g, "-"));
+
+/**
+ * Holds the search, sort, and niche for the directory, so the hero search and the grid below it
+ * share them. The static page renders the default view; a shared link's `?q=`, `?sort=`, and
+ * `?niche=` are read in the browser after hydration.
+ */
+export function DirectoryView({ children, niches, sorts }: { children: ReactNode; niches: readonly DirectoryNiche[]; sorts: readonly AdNetworkDirectorySort[] }) {
+ const [view, setView] = useState({ q: "", sort: "most-sites", niche: "" });
+
+ useEffect(() => {
+ const read = () => {
+ const search = new URLSearchParams(window.location.search);
+ const sort = search.get("sort") as AdNetworkDirectorySort | null;
+ const niche = search.get("niche") ?? "";
+ setView({
+ q: cleanQuery(search.get("q") ?? ""),
+ sort: sort && sorts.includes(sort) ? sort : "most-sites",
+ niche: niches.some((item) => item.slug === niche) ? niche : "",
+ });
+ };
+ if (window.location.search) read();
+ window.addEventListener("popstate", read);
+ return () => window.removeEventListener("popstate", read);
+ }, [niches, sorts]);
+
+ const value = useMemo(() => ({
+ view,
+ set: (patch: Partial) => {
+ const next = { ...view, ...patch };
+ const search = new URLSearchParams();
+ if (cleanQuery(next.q)) search.set("q", cleanQuery(next.q));
+ if (next.sort !== "most-sites") search.set("sort", next.sort);
+ if (next.sort === "by-niche" && next.niche) search.set("niche", next.niche);
+ const suffix = search.toString();
+ window.history.replaceState(null, "", suffix ? `${window.location.pathname}?${suffix}` : window.location.pathname);
+ setView(next);
+ },
+ }), [view]);
+
+ return {children};
+}
+
+function useView() {
+ const context = useContext(ViewContext);
+ if (!context) throw new Error("DirectoryView is missing");
+ return context;
+}
+
+/**
+ * The hero search (mockup `.hero-search`). Typing filters the network cards below. Submitting a
+ * domain opens the checker, a niche name opens its page, and a single matching network opens its
+ * page. `/` jumps to the box.
+ */
+export function HeroSearch({ examples, networks, niches, placeholder }: {
+ examples: readonly { label: string; href: string }[];
+ networks: readonly DirectoryNetwork[];
+ /** Published niche pages only. */
+ niches: readonly { slug: string; name: string }[];
+ placeholder: string;
+}) {
+ const router = useRouter();
+ const { view, set } = useView();
+ const input = useRef(null);
+
+ useEffect(() => {
+ const onKey = (event: KeyboardEvent) => {
+ const active = document.activeElement;
+ if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return;
+ if (active instanceof HTMLElement && (active.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName))) return;
+ event.preventDefault();
+ input.current?.focus();
+ };
+ document.addEventListener("keydown", onKey);
+ return () => document.removeEventListener("keydown", onKey);
+ }, []);
+
+ const submit = (event: FormEvent) => {
+ event.preventDefault();
+ const text = cleanQuery(view.q);
+ if (!text) return;
+ const lower = text.toLowerCase();
+ const named = networks.filter((network) => matches(network, lower));
+ if (domainLike.test(text) && !named.length) {
+ router.push(checkerHref(text.replace(/^https?:\/\//i, "").split(/[/?#]/)[0].replace(/^www\./i, "").toLowerCase()));
+ return;
+ }
+ const niche = niches.find((item) => item.name.toLowerCase() === lower || item.slug === lower);
+ if (niche) {
+ router.push(`/ad-networks/niches/${niche.slug}`);
+ return;
+ }
+ if (named.length === 1) router.push(`/ad-networks/${named[0].slug}`);
+ else document.getElementById("all-networks")?.scrollIntoView({ block: "start" });
+ };
+
+ return (
+
+ );
+}
+
+/**
+ * "All ad networks": the sort controls, the sort note, and the cards. Every card is in the server
+ * HTML in the Most sites order. Cards past the first twelve are `hidden="until-found"`, so search
+ * engines and find-in-page still reach them, until "Show all" opens them.
+ */
+export function NetworkBrowser({ cards, easiest, easiestNote, networks, niches, sorts }: {
+ /** One rendered card per network slug. */
+ cards: Record;
+ /** Network slugs with a known traffic minimum, lowest first. */
+ easiest: readonly string[];
+ easiestNote: ReactNode;
+ /** In Most sites order. */
+ networks: readonly DirectoryNetwork[];
+ niches: readonly DirectoryNiche[];
+ sorts: readonly AdNetworkDirectorySort[];
+}) {
+ const { view, set } = useView();
+ const [expanded, setExpanded] = useState(false);
+ const [hydrated, setHydrated] = useState(false);
+ const grid = useRef(null);
+ const needle = cleanQuery(view.q).toLowerCase();
+ const niche = view.sort === "by-niche" ? niches.find((item) => item.slug === view.niche) ?? niches[0] : undefined;
+
+ const list = useMemo(() => {
+ const ranked = (order: readonly string[]) => {
+ const position = new Map(order.map((slug, index) => [slug, index]));
+ // Networks missing from the order keep their Most sites order after the ranked ones.
+ return [...networks].sort((a, b) => (position.get(a.slug) ?? Infinity) - (position.get(b.slug) ?? Infinity));
+ };
+ const ordered = view.sort === "easiest-to-join" ? ranked(easiest) : niche ? ranked(niche.order) : networks;
+ return needle ? ordered.filter((network) => matches(network, needle)) : ordered;
+ }, [easiest, needle, networks, niche, view.sort]);
+
+ const collapsed = !expanded && !needle && list.length > visibleAtFirst;
+
+ // Find-in-page on a hidden card opens the whole list.
+ useEffect(() => {
+ const root = grid.current;
+ if (!root || !collapsed) return;
+ const open = () => setExpanded(true);
+ root.addEventListener("beforematch", open);
+ return () => root.removeEventListener("beforematch", open);
+ }, [collapsed]);
+
+ // React can't render `hidden="until-found"`, so the server HTML hides the extra cards with CSS
+ // and the browser swaps that for the attribute before the first paint.
+ useLayoutEffect(() => setHydrated(true), []);
+ useLayoutEffect(() => {
+ if (!hydrated) return;
+ grid.current?.querySelectorAll("[data-card]").forEach((card, index) => {
+ if (collapsed && index >= visibleAtFirst) card.setAttribute("hidden", "until-found");
+ else card.removeAttribute("hidden");
+ });
+ }, [collapsed, hydrated, list]);
+
+ return (
+
+ 1 ? (
+
+ set({ sort: value as AdNetworkDirectorySort, niche: value === "by-niche" ? niche?.slug ?? niches[0]?.slug ?? "" : "" })}
+ options={sorts.map((sort) => ({ value: sort, label: sortLabels[sort] }))}
+ value={view.sort}
+ />
+ {niche ? (
+ set({ niche: value })} options={niches.map((item) => ({ value: item.slug, label: item.name }))} value={niche.slug} />
+ ) : null}
+
+ ) : null}
+ id="all-networks"
+ title="All ad networks"
+ />
+ {view.sort === "easiest-to-join" ? {easiestNote} : null}
+ {niche ? Sorted by how many {niche.name} sites use each network. : null}
+ {list.length ? (
+
+
+ {list.map((network) =>
{cards[network.slug]}
)}
+
+
+ ) : (
+ set({ q: "" })}>Show all networks} title="No networks match that search">
+
Try a shorter name, or press Search to check a site's domain.
+
+ )}
+ {collapsed ? (
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/app/(site)/(directories)/ad-networks/directory.module.css b/app/(site)/(directories)/ad-networks/directory.module.css
new file mode 100644
index 00000000..ec2012da
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/directory.module.css
@@ -0,0 +1,257 @@
+/* The ad networks directory (mockup ad-networks.html) and the header parts the network pages share. */
+
+/* Header */
+
+.header > p {
+ max-width: var(--measure-prose);
+}
+
+.metaRow {
+ align-items: center;
+ color: var(--color-text-muted);
+ display: flex;
+ flex-wrap: wrap;
+ font: var(--text-small) / 1.4 var(--font-mono);
+ gap: var(--space-2) var(--space-7);
+}
+
+.official {
+ align-items: center;
+ color: inherit;
+ display: inline-flex;
+ gap: var(--space-2);
+ text-decoration: underline;
+ text-decoration-color: var(--color-border);
+ text-underline-offset: 3px;
+}
+
+.official svg {
+ height: 15px;
+ width: 15px;
+}
+
+.official:hover,
+.official:focus-visible {
+ color: var(--color-text);
+ text-decoration-color: currentColor;
+}
+
+.visuallyHidden {
+ clip-path: inset(50%);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+
+/* Hero search: one box for networks, sites, and niches */
+
+.heroSearch {
+ display: grid;
+ gap: var(--space-4);
+ margin-top: var(--space-5);
+ max-width: var(--measure-prose);
+}
+
+.heroSearch form {
+ display: grid;
+ gap: var(--space-3);
+ grid-template-columns: minmax(0, 1fr) auto;
+}
+
+.searchBox {
+ align-items: center;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border-strong);
+ border-radius: var(--radius);
+ display: flex;
+ height: 52px;
+ min-width: 0;
+}
+
+.searchBox:focus-within {
+ border-color: var(--color-accent-text);
+ box-shadow: var(--focus-ring);
+}
+
+.searchBox svg {
+ color: var(--color-text-muted);
+ flex: none;
+ height: 20px;
+ margin-left: 14px;
+ width: 20px;
+}
+
+.searchBox input {
+ background: transparent;
+ border: 0;
+ color: var(--color-text);
+ flex: 1;
+ font: var(--text-body) var(--font-mono);
+ height: 100%;
+ min-width: 0;
+ outline: 0;
+ padding: 0 14px;
+}
+
+.searchBox input:focus-visible {
+ box-shadow: none;
+}
+
+.searchBox input::placeholder {
+ color: var(--color-text-muted);
+}
+
+.searchBox kbd {
+ border: 1px solid var(--color-border-strong);
+ border-radius: var(--radius);
+ color: var(--color-text-muted);
+ font: var(--text-small) var(--font-mono);
+ margin-right: 12px;
+ padding: 1px 7px;
+}
+
+.primaryButton,
+.secondaryButton {
+ align-items: center;
+ border-radius: var(--radius);
+ cursor: pointer;
+ display: inline-flex;
+ font: var(--weight-semibold) var(--text-body) var(--font-sans);
+ justify-content: center;
+ padding: 0 22px;
+ white-space: nowrap;
+}
+
+.primaryButton {
+ background: var(--color-accent);
+ border: 1px solid var(--color-accent);
+ color: var(--color-accent-contrast);
+ height: 52px;
+}
+
+.primaryButton:hover {
+ filter: brightness(0.96);
+}
+
+.secondaryButton {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border-strong);
+ color: var(--color-text);
+ height: var(--control-height-lg);
+}
+
+.secondaryButton:hover {
+ background: var(--color-surface-2);
+}
+
+.primaryButton:focus-visible,
+.secondaryButton:focus-visible {
+ box-shadow: var(--focus-ring);
+ outline: 0;
+}
+
+.searchTry {
+ align-items: baseline;
+ background: var(--color-bg);
+ color: var(--color-text-muted);
+ display: flex;
+ flex-wrap: wrap;
+ font: var(--text-small) / var(--line-ui) var(--font-mono);
+ gap: var(--space-2) var(--space-5);
+ margin: 0;
+ width: fit-content;
+}
+
+.searchTry a {
+ color: var(--color-text);
+ text-decoration: underline;
+ text-decoration-color: var(--color-border-strong);
+ text-underline-offset: 3px;
+}
+
+.searchTry a:hover {
+ text-decoration-color: currentColor;
+}
+
+@media (max-width: 560px) {
+ .heroSearch form {
+ grid-template-columns: 1fr;
+ }
+
+ .searchBox kbd {
+ display: none;
+ }
+}
+
+/* Body: sections spaced like the mockup's separate canvases, with no rules between them */
+
+.body {
+ display: grid;
+ min-width: 0;
+}
+
+.body > * {
+ min-width: 0;
+}
+
+.body > * + * {
+ margin-top: calc(var(--page-padding-top) + var(--page-padding-bottom));
+}
+
+/* Network cards */
+
+.directory {
+ min-width: 0;
+}
+
+.cell {
+ display: grid;
+ min-width: 0;
+}
+
+.cards[data-collapsed] .cell:nth-child(n + 13) {
+ display: none;
+}
+
+/* `hidden="until-found"` keeps a box, so take hidden cards out of the grid flow. Find-in-page
+ still reaches them and opens the list. */
+.cell[hidden] {
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ width: 1px;
+}
+
+.cards {
+ position: relative;
+}
+
+.more {
+ display: flex;
+ justify-content: center;
+ margin-top: var(--space-8);
+}
+
+/* Reading content under the cards (mockup `.prose`) */
+
+.article {
+ display: grid;
+ gap: var(--space-7);
+ max-width: var(--measure-prose);
+}
+
+.article h2 {
+ font: var(--weight-bold) 1.7rem / 1.2 var(--font-display);
+ letter-spacing: var(--tracking-display);
+ margin: calc(var(--space-13) - var(--space-7)) 0 0;
+}
+
+.article > h2:first-child {
+ margin-top: 0;
+}
+
+.article p {
+ margin: 0;
+}
diff --git a/app/(site)/(directories)/ad-networks/join-rules.ts b/app/(site)/(directories)/ad-networks/join-rules.ts
new file mode 100644
index 00000000..6f80644d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/join-rules.ts
@@ -0,0 +1,78 @@
+// How the ad network pages word joining rules. Pure, so the wording can be checked against every network.
+import { adNetworkFacts, type AdNetworkFacts } from "./network-facts";
+
+export const NOT_PUBLIC = "Not public";
+
+export function networkFacts(slug: string): AdNetworkFacts | null {
+ return adNetworkFacts[slug] ?? null;
+}
+
+/** The traffic rule as a short phrase, or null when the network publishes none. */
+export function trafficRule(facts: AdNetworkFacts) {
+ const rule = facts.minTraffic;
+ if (!rule) return null;
+ if (rule.amount === 0) return "No traffic minimum";
+ return `${rule.amount.toLocaleString("en-US")} ${rule.unit} a month`;
+}
+
+// Key numbers about pay, fees, or contracts. Everything else is a rule for joining.
+const payNumber = /pay|share|fee|payout|terms|threshold|notice|timing|schedule|floor|minimums?$/i;
+
+/** Key numbers that say what it takes to join, such as revenue or traffic to apply. */
+export function joiningNumbers(facts: AdNetworkFacts) {
+ return facts.keyNumbers.filter((item) => !payNumber.test(item.label));
+}
+
+// Notes that say the number isn't a rule anyone has to meet.
+const notARule = /not a general minimum|not the main rule/i;
+
+/**
+ * A joining key number as one phrase: the value, then the first part of its note, worded the
+ * way people say it. "$5,000" with "in annual ad revenue" reads "$5,000 ad revenue a year".
+ */
+export function joinPhrase(item: AdNetworkFacts["keyNumbers"][number]) {
+ let unit = (item.note ?? "").split(";")[0].trim();
+ // "monthly ad revenue or 2 million monthly pageviews": keep the first rule.
+ unit = unit.replace(/\s+or\s+(?=[^,]*\d).*$/i, "");
+ unit = unit.replace(/,.*$/, "").replace(/^in\s+/i, "");
+ unit = unit.replace(/^monthly\s+(.+)$/i, "$1 a month").replace(/^annual\s+(.+)$/i, "$1 a year");
+ const phrase = unit ? `${item.value} ${unit}` : item.value;
+ return /^(suggested|typical)/i.test(item.label) ? `Typically ${phrase.charAt(0).toLowerCase()}${phrase.slice(1)}` : phrase;
+}
+
+/** What the card shows under "To join": the rule, "Not public", or "Not listed" before research. */
+export function joinLabel(slug: string): { value: string } {
+ const facts = networkFacts(slug);
+ if (!facts) return { value: "Not listed" };
+ const rule = trafficRule(facts);
+ if (rule) return { value: rule };
+ const first = joiningNumbers(facts).find((item) => !notARule.test(item.note ?? ""));
+ return { value: first ? joinPhrase(first) : NOT_PUBLIC };
+}
+
+/**
+ * The rule a network publishes for joining, as a phrase for the header lede, such as
+ * `50,000 sessions a month` or `$5,000 ad revenue a year`. Null when it publishes none.
+ */
+export function joinRule(slug: string): string | null {
+ const facts = networkFacts(slug);
+ if (!facts) return null;
+ const rule = trafficRule(facts);
+ if (rule) return rule;
+ const first = joiningNumbers(facts).find((item) => !notARule.test(item.note ?? ""));
+ return first ? joinPhrase(first) : null;
+}
+
+/** The header sentence for a join rule: `It takes sites with 50,000 sessions a month.` */
+export function joinSentence(rule: string) {
+ if (rule === "No traffic minimum") return "It takes sites of any size.";
+ const typical = /^Typically\s+/i.test(rule);
+ const phrase = rule.replace(/^Typically\s+/i, "");
+ const words = /^[A-Z][a-z]/.test(phrase) ? phrase.charAt(0).toLowerCase() + phrase.slice(1) : phrase;
+ return `It ${typical ? "typically " : ""}takes sites with ${words}.`;
+}
+
+/** Monthly traffic needed, for ordering by how easy a network is to join. Unknown sorts last. */
+export function joinOrder(slug: string) {
+ return networkFacts(slug)?.minTraffic?.amount ?? null;
+}
diff --git a/app/(site)/(directories)/ad-networks/network-facts.ts b/app/(site)/(directories)/ad-networks/network-facts.ts
new file mode 100644
index 00000000..101dcf26
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/network-facts.ts
@@ -0,0 +1,70 @@
+// Network facts that change rarely: joining rules and pay terms, with sources.
+// The page shows these numbers; the MDX words never repeat them. Use null for
+// anything the network does not publish, and the page shows "Not public".
+
+export type AdNetworkKeyNumber = {
+ label: string; // "Sessions to join", "Your share", "Paid"
+ value: string; // "50,000", "75%", "Net 65"
+ note?: string; // "a month", "of ad revenue"
+ sourceUrl: string;
+};
+
+export type AdNetworkFacts = {
+ checkedAt: string; // YYYY-MM-DD, the day the sources were read
+ keyNumbers: AdNetworkKeyNumber[]; // up to three, most useful first
+ joining: { label: string; value: string; note?: string; sourceUrl: string | null }[];
+ minTraffic: { amount: number; unit: "sessions" | "pageviews" | "visitors" } | null; // monthly; null when there is no public rule
+ revenueShare: string | null;
+ paymentTerms: string | null;
+ paymentThreshold: string | null;
+ paymentMethods: string[];
+ sources: { label: string; url: string }[];
+};
+
+// Keyed by network slug.
+export const adNetworkFacts: Record = {
+ adapex: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 60", sourceUrl: "https://adapex.io/faqs/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Google Ad Manager via MCM", sourceUrl: "https://adapex.io/faqs/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Publisher FAQ", url: "https://adapex.io/faqs/" }] },
+ aditude: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Official publisher guidance", url: "https://www.aditude.com/blog/the-5-questions-every-publisher-should-ask-their-monetization-partner" }] },
+ adnimation: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 35", sourceUrl: "https://www.adnimation.com/faqs/" }], joining: [{ label: "Traffic", value: "Substantial, no figure stated", note: "MAP is a separate program with no minimum", sourceUrl: "https://www.adnimation.com/faqs/" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No gambling, pornography, or drugs", sourceUrl: "https://www.adnimation.com/faqs/" }, { label: "Google standing", value: "Follow Google policy guidelines", sourceUrl: "https://www.adnimation.com/faqs/" }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 35", paymentThreshold: null, paymentMethods: ["Direct bank payment", "PayPal by arrangement"], sources: [{ label: "Publisher FAQ", url: "https://www.adnimation.com/faqs/" }, { label: "MAP program terms, separate program", url: "https://www.adnimation.com/map-terms-conditions/" }] },
+ adpushup: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "80%", note: "in older public terms; check your signed agreement", sourceUrl: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }, { label: "Payment minimum", value: "$50", note: "in older public terms", sourceUrl: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }, { label: "Pay day", value: "Net 45", note: "in older public terms", sourceUrl: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "80% in older published terms; current agreement may differ", paymentTerms: "Net 45 in older published terms", paymentThreshold: "$50 in older published terms", paymentMethods: [], sources: [{ label: "Publisher terms, revised 2018", url: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }, { label: "Publisher onboarding", url: "https://www.adpushup.com/publisher/get-started/" }] },
+ "amazon-publisher-services": { checkedAt: "2026-10-16", keyNumbers: [{ label: "UAM fee", value: "10%", note: "taken from SSP bid prices; Unified Ad Marketplace only", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Payment minimum", value: "$5", note: "UAM", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Pay day", value: "Net 60", note: "UAM", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Worldwide", note: "For Unified Ad Marketplace only", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Google Ad Manager", note: "For Unified Ad Marketplace only", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60 for UAM", paymentThreshold: "$5 for UAM", paymentMethods: [], sources: [{ label: "UAM publisher update", url: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }] },
+ "anymind-anymanager": { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "AnyManager overview", url: "https://anymindgroup.com/news/press-release/11388/" }, { label: "AnyMind IR FAQ", url: "https://anymindgroup.com/ir/faq/" }] },
+ "buysellads-carbon-ads": { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Carbon Tag or API", sourceUrl: "https://www.buysellads.com/publishers" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Carbon Ads media kit", url: "https://www.buysellads.com/hubfs/Carbon%20Ads%20Media%20Kit.pdf" }, { label: "Carbon Ads publisher page", url: "https://discover.buysellads.com/carbon" }] },
+ clickio: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "100,000 to 1,000,000", note: "monthly pageviews; varies by region, and revenue can also qualify a site", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Minimum earnings", value: "$500", note: "a month; may be enforced after the first three months", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Pay day", value: "Net 60", sourceUrl: "https://docs.clickio.com/books/get-started/page/payments-billing" }], joining: [{ label: "Traffic", value: "100,000 to 1,000,000 pageviews by region", note: "Monthly; $500 in monthly revenue may qualify below the threshold", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Visitor location", value: "Sanctioned countries excluded", note: "Pageview thresholds also vary by region", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Content", value: "Follow AdSense policies and third-party rights rules", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Publish ads.txt; provide GA4 or a six-month report if requested", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Site history", value: "Six months of traffic data if external verification is unavailable", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Ad revenue", value: "$500 a month", note: "Possible traffic alternative; ongoing minimum account earnings", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60", paymentThreshold: "Varies by method: $100 equivalent for ACH/eCheck/SEPA, PayPal and check; $200 wire; some other methods have separate thresholds", paymentMethods: ["ACH", "eCheck", "SEPA", "Wire", "Check", "PayPal", "WebMoney", "Payoneer"], sources: [{ label: "Entry standards", url: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Payments and billing", url: "https://docs.clickio.com/books/get-started/page/payments-billing" }] },
+ concert: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Concert terms surfaced publicly (advertiser product, not publisher terms)", url: "https://admanager.concert.io/terms" }] },
+ ethicalads: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "70%", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Payment minimum", value: "$50", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Pay day", value: "Monthly", note: "target by the 15th", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }], joining: [{ label: "Traffic", value: "No minimum stated", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Meet the publisher policy", note: "Ads must appear above the fold on desktop and mobile", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "70%", paymentTerms: "Monthly; target payment date is the 15th", paymentThreshold: "$50", paymentMethods: ["Stripe Connect / bank transfer", "PayPal", "OpenCollective", "GitHub Sponsors"], sources: [{ label: "Publisher policy", url: "https://www.ethicalads.io/publisher-policy/" }, { label: "Terms of service", url: "https://www.ethicalads.io/terms-of-service/" }] },
+ ezoic: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Users to join", value: "250,000", note: "monthly active users, for new sites", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Your share", value: "82%", note: "at least, of net ad revenue", sourceUrl: "https://www.ezoic.com/terms" }, { label: "Payment minimum", value: "$20", sourceUrl: "https://www.ezoic.com/terms" }], joining: [{ label: "Traffic", value: "250,000 monthly active users", note: "Incubator may accept smaller sites", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original content in a supported language", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Google standing", value: "Comply with Google and AdSense policies", note: "AdSense approval is not required for most sites", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Must connect", value: "Google Analytics for traffic verification", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 250000, unit: "visitors" }, revenueShare: "At least 82% of net ad revenue under published Terms, unless another agreement applies", paymentTerms: "Approximately 35 days after the advertiser pays, under published Terms", paymentThreshold: "$20", paymentMethods: ["PayPal", "Payoneer bank transfer", "Check"], sources: [{ label: "Current eligibility requirements", url: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Terms", url: "https://www.ezoic.com/terms" }, { label: "Payment help", url: "https://support.ezoic.com/kb/article/ezoic-payments?lang=en-US&lang=it%3Fid%3Dezoic-payments" }] },
+ freestar: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "1,000,000", note: "monthly pageviews; Freestar says usually, so confirm with its team", sourceUrl: "https://freestar.com/contact-us/" }, { label: "From top markets", value: "50%", note: "of traffic from the US, UK, Canada, New Zealand, or Australia", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }], joining: [{ label: "Traffic", value: "Usually 1,000,000 pageviews", note: "Monthly", sourceUrl: "https://freestar.com/contact-us/" }, { label: "Visitor location", value: "At least 50% from the US, UK, Canada, New Zealand, or Australia", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Content", value: "Original, human-focused, brand-safe content", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Google standing", value: "Good standing with major ad providers", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Must connect", value: "Provide GA4 access for review", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 1000000, unit: "pageviews" }, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Publisher quality requirements", url: "https://freestar.com/publisher-quality-requirements/" }, { label: "Publisher contact", url: "https://freestar.com/contact-us/" }] },
+ "geniee-ssp": { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "GENIEE corporate site", url: "https://geniee.co.jp/" }, { label: "GENIEE SSP site", url: "https://genieesspv.jp/" }] },
+ "google-adsense": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "80%", note: "after the advertiser platform fee; about 68% with Google Ads demand", sourceUrl: "https://support.google.com/adsense/answer/180195?hl=en" }, { label: "Payment minimum", value: "$100", note: "in US dollars", sourceUrl: "https://support.google.com/adsense/answer/1709871?hl=en" }, { label: "Pay days", value: "21st to 26th", note: "of each month", sourceUrl: "https://support.google.com/adsense/answer/7164701?hl=en" }], joining: [{ label: "Traffic", value: "No minimum stated", sourceUrl: "https://support.google.com/adsense/answer/9724?hl=en" }, { label: "Visitor location", value: "Sign-up is available in listed countries", sourceUrl: "https://support.google.com/adsense/answer/13402307?hl=en" }, { label: "Content", value: "Original, high-quality, policy-compliant content", sourceUrl: "https://support.google.com/adsense/answer/9724?hl=en" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Access to the site HTML", note: "Needed to verify and place ad code", sourceUrl: "https://support.google.com/adsense/answer/9724?hl=en" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "80% after the advertiser platform fee for AdSense for Content", paymentTerms: "Monthly; payments are issued between the 21st and 26th when the balance reaches threshold and there are no holds", paymentThreshold: "$100 USD, with equivalent thresholds by account currency", paymentMethods: ["Check", "EFT", "SEPA", "Wire transfer", "PayPal Hyperwallet where available"], sources: [{ label: "Revenue share", url: "https://support.google.com/adsense/answer/180195?hl=en" }, { label: "Payment threshold", url: "https://support.google.com/adsense/answer/1709871?hl=en" }, { label: "Payment schedule", url: "https://support.google.com/adsense/answer/7164701?hl=en" }, { label: "Eligibility", url: "https://support.google.com/adsense/answer/9724?hl=en" }] },
+ infolinks: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "70%", note: "for In-Text ads", sourceUrl: "https://sites0001.infolinks.com/service-agreement?email=&name=&url=" }, { label: "Pay day", value: "Net 45", note: "working days", sourceUrl: "https://sites0001.infolinks.com/service-agreement?email=&name=&url=" }, { label: "Payment minimum", value: "$50", note: "for eCheck", sourceUrl: "https://sites0001.infolinks.com/support/payments/can-i-receive-my-payment-via-echeck" }], joining: [{ label: "Traffic", value: "No minimum pageviews or visitors", sourceUrl: "https://sites0001.infolinks.com/support/quality/what-are-the-eligibility-criteria-for-my-site-to-qualify-with-infolinks" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No illegal or offensive content", sourceUrl: "https://sites0001.infolinks.com/support/quality/what-are-the-eligibility-criteria-for-my-site-to-qualify-with-infolinks" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Install the Infolinks script", sourceUrl: "https://sites0001.infolinks.com/support/integration/how-do-i-integrate-infolinks-into-my-website" }, { label: "Approval time", value: "Usually 48 hours", sourceUrl: "https://sites0001.infolinks.com/support/account/how-do-i-sign-up-as-an-infolinks-publisher" }], minTraffic: null, revenueShare: "70% for In-Text ad units; other format terms vary", paymentTerms: "Net 45 working days after the month ends under the public agreement", paymentThreshold: "Varies by method; eCheck $50", paymentMethods: ["ACH", "eCheck", "Payoneer", "PayPal", "Western Union"], sources: [{ label: "Publisher eligibility", url: "https://sites0001.infolinks.com/support/quality/what-are-the-eligibility-criteria-for-my-site-to-qualify-with-infolinks" }, { label: "Service agreement", url: "https://sites0001.infolinks.com/service-agreement?email=&name=&url=" }, { label: "Payment options", url: "https://sites0001.infolinks.com/support/payments/do-i-pay-fees-when-receiving-my-infolinks-payment" }] },
+ "journey-by-mediavine": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "More than 1,000", note: "monthly sessions", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }], joining: [{ label: "Traffic", value: "More than 1,000 sessions", note: "Monthly", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original, updated, human-created, brand-safe content", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Google standing", value: "No active AdSense or Ad Exchange issues", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Must connect", value: "Install the Grow plugin", note: "Journey requirement", sourceUrl: "https://www.journeymv.com/affiliates-faq/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 1000, unit: "sessions" }, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Mediavine approval requirements, including Journey", url: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Journey criteria", url: "https://www.journeymv.com/affiliates-faq/" }] },
+ "media-net": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 30", sourceUrl: "https://www.media.net/legal/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original content; prohibited topics are excluded", sourceUrl: "https://www.media.net/legal/inventorypolicies/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Publish an authorized ads.txt file", sourceUrl: "https://www.media.net/legal/inventorypolicies/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 30; monthly revenue payments under the Publisher Agreement", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Publisher Agreement", url: "https://www.media.net/legal/" }, { label: "Inventory policies", url: "https://www.media.net/legal/inventorypolicies/" }] },
+ mediavine: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Revenue to join", value: "$5,000", note: "in annual ad revenue", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Your share", value: "75%", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Pay day", value: "Net 65", sourceUrl: "https://help.mediavine.com/mediavine-payment-faq" }], joining: [{ label: "Traffic", value: "No traffic minimum listed", note: "Mediavine sets an ad revenue minimum instead", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original, quality, evergreen content", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Google standing", value: "No active AdSense ban", note: "You do not need prior AdSense use", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Ad revenue", value: "$5,000 a year", note: "Minimum annual ad revenue", sourceUrl: "https://help.mediavine.com/mediavine-official" }], minTraffic: null, revenueShare: "75%", paymentTerms: "Net 65; paid on the 5th or first business day after it", paymentThreshold: "$100 for most methods; higher for some international methods", paymentMethods: ["PayPal", "Domestic ACH", "International ACH", "US wire", "International wire"], sources: [{ label: "Mediavine Official requirements", url: "https://help.mediavine.com/mediavine-official" }, { label: "Payment FAQ", url: "https://help.mediavine.com/mediavine-payment-faq" }, { label: "Payment methods and minimums", url: "https://help.mediavine.com/mediavine-payment-methods" }] },
+ mgid: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "65%", note: "on a published regional offer; current order terms may differ", sourceUrl: "https://pr.mgid.com/monetize" }, { label: "Pay day", value: "Net 30", note: "on the same offer", sourceUrl: "https://pr.mgid.com/monetize" }, { label: "Payment minimums", value: "$50 to $1,000", note: "varies by payment method", sourceUrl: "https://pr.mgid.com/monetize" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No adult, illegal, hateful, or infringing content", sourceUrl: "https://www.mgid.com/services/publishers-tos" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Install MGID ad units and code", sourceUrl: "https://www.mgid.com/services/publishers-tos" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 30 in regional offer; current Publisher Terms and order control", paymentThreshold: "Method-specific; published offer lists $50 to $1,000", paymentMethods: ["Bank transfer", "PayPal", "Tipalti", "Paymaster24"], sources: [{ label: "Regional publisher offer", url: "https://pr.mgid.com/monetize" }, { label: "Publisher terms", url: "https://www.mgid.com/services/publishers-tos" }] },
+ "monetizemore-pubguru": { checkedAt: "2026-10-16", keyNumbers: [{ label: "To join Premium", value: "$5,000", note: "monthly ad revenue or 2 million monthly pageviews", sourceUrl: "https://www.monetizemore.com/solutions/pubguru-premium/" }], joining: [{ label: "Traffic", value: "2,000,000 pageviews", note: "Monthly, an alternative to the revenue threshold", sourceUrl: "https://www.monetizemore.com/solutions/pubguru-premium/" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Ad revenue", value: "$5,000 a month", note: "Alternative to the 2 million monthly pageview threshold", sourceUrl: "https://www.monetizemore.com/solutions/pubguru-premium/" }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "PubGuru Premium", url: "https://www.monetizemore.com/solutions/pubguru-premium/" }, { label: "MonetizeMore FAQ", url: "https://www.monetizemore.com/frequently-asked-questions/" }] },
+ monumetric: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "10,000", note: "monthly pageviews, Propel program", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Pay day", value: "Net 60", sourceUrl: "https://www.monumetric.com/payment-terms-and-how-you-are-paid/" }, { label: "Payment minimum", value: "$10", sourceUrl: "https://www.monumetric.com/payment-terms-and-how-you-are-paid/" }], joining: [{ label: "Traffic", value: "At least 10,000 pageviews", note: "Monthly", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Visitor location", value: "At least 50% from the US, UK, Canada, or Australia", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Content", value: "English; no pornography, drugs, hate speech, or copyrighted content", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 10000, unit: "pageviews" }, revenueShare: null, paymentTerms: "Net 60; paid monthly within the first ten business days", paymentThreshold: "$10; balance rolls forward below this amount", paymentMethods: ["US direct deposit", "International PayPal", "Payoneer in some cases"], sources: [{ label: "Eligibility requirements", url: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Payment terms", url: "https://www.monumetric.com/payment-terms-and-how-you-are-paid/" }] },
+ "newor-media": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic for Catapult", value: "60,000", note: "US pageviews a month", sourceUrl: "https://newormedia.com/programs" }, { label: "Pay day", value: "Net 45", note: "in the platform agreement; the FAQ says Net 30", sourceUrl: "https://newormedia.com/node/296" }], joining: [{ label: "Traffic", value: "No minimum for Elevate", note: "Catapult: 60,000 US pageviews; Premier: 420,000 US pageviews monthly", sourceUrl: "https://newormedia.com/programs" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "English site with navigation; follow Google ad policies", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Google standing", value: "Follow Google ad policies", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Must connect", value: "Google Ad Manager account", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Approval time", value: "Usually 24 to 48 hours", note: "For Elevate review", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Exclusivity", value: "Required", note: "Newor Media says all publishers must be exclusive", sourceUrl: "https://newormedia.com/elevatefaqs" }], minTraffic: null, revenueShare: null, paymentTerms: "Net 45 in platform agreement; FAQ says Net 30", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Programs", url: "https://newormedia.com/programs" }, { label: "Elevate FAQ", url: "https://newormedia.com/elevatefaqs" }, { label: "Platform agreement", url: "https://newormedia.com/node/296" }, { label: "Payment FAQ", url: "https://newormedia.com/faqs/what-are-your-payment-terms" }] },
+ nitropay: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "None", note: "above the transaction fee", sourceUrl: "https://nitropay.com/docs/" }, { label: "Pay day", value: "Net 7", sourceUrl: "https://nitropay.com/docs/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 7 per public docs archive", paymentThreshold: "No stated minimum; earnings must exceed transaction fee", paymentMethods: [], sources: [{ label: "NitroPay docs archive", url: "https://nitropay.com/docs/" }, { label: "Bonus share terms", url: "https://nitropay.com/bonus/" }] },
+ "opti-digital": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day, Managed Inventory", value: "Net 45", sourceUrl: "https://knowledge-base.optidigital.com/knowledge/understanding-your-payment-terms" }, { label: "Pay day, Managed Account", value: "Net 60", note: "for applicable demand", sourceUrl: "https://knowledge-base.optidigital.com/knowledge/understanding-your-payment-terms" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 45 for Managed Inventory; Net 60 for applicable demand under Managed Account; exact setup is in the order", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Payment terms by setup", url: "https://knowledge-base.optidigital.com/knowledge/understanding-your-payment-terms" }, { label: "Invoice flow", url: "https://knowledge-base.optidigital.com/knowledge/how-is-the-invoicing-working-at-opti-digital-when-will-i-get-paid" }] },
+ playwire: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "100,000 to 500,000", note: "monthly pageviews; Playwire's pages differ", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Pay day", value: "Net 60", sourceUrl: "https://www.playwire.com/faq" }], joining: [{ label: "Traffic", value: "500,000 monthly pageviews or 1,000 daily active users", note: "Qualification page separately suggests applying from 100,000 monthly pageviews", sourceUrl: "https://www.playwire.com/faq" }, { label: "Visitor location", value: "At least 20% of traffic from the US", note: "Qualification guidance", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Content", value: "Unique, original content", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Google standing", value: "Not blacklisted by Google or major ad providers", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Must connect", value: "Implement the Playwire SDK", note: "Required to access its demand", sourceUrl: "https://www.playwire.com/faq" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Qualification criteria", url: "https://www.playwire.com/qualification-criteria" }, { label: "Publisher FAQ", url: "https://www.playwire.com/faq" }] },
+ pubfuture: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 30", sourceUrl: "https://pubfuture.com/?from=chapterpage" }, { label: "Payment minimum", value: "$50", sourceUrl: "https://pubfuture.com/?from=chapterpage" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 30", paymentThreshold: "$50", paymentMethods: ["PayPal", "Payoneer", "Bank transfer", "Bitcoin"], sources: [{ label: "Publisher FAQ", url: "https://pubfuture.com/?from=chapterpage" }, { label: "PubFuture publisher case study", url: "https://blog.pubfuture.com/the-secret-to-400-more-revenue-for-h5-game-sites-pubfutures-winning-formula/1007/" }] },
+ pubgalaxy: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Suggested traffic", value: "100,000", note: "monthly pageviews; not a hard minimum", sourceUrl: "https://www.pubgalaxy.com/learn-center/" }, { label: "Your share", value: "85%", note: "at least, of partner net revenue; at least 65% of direct net revenue", sourceUrl: "https://www.pubgalaxy.com/terms-and-conditions/" }, { label: "Payment minimum", value: "$50", note: "PayPal; $100 wire", sourceUrl: "https://www.pubgalaxy.com/terms-and-conditions/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "At least 85% of net revenue from ad partners and 65% from direct deals under published terms", paymentTerms: "Within 35 days after month end, subject to PubGalaxy receiving advertiser funds", paymentThreshold: "$50 PayPal; $100 wire", paymentMethods: ["PayPal", "Wire transfer"], sources: [{ label: "Publisher Learn Center", url: "https://www.pubgalaxy.com/learn-center/" }, { label: "Publisher terms", url: "https://www.pubgalaxy.com/terms-and-conditions/" }] },
+ publift: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Revenue to join", value: "More than $2,000", note: "monthly ad revenue, typically", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Pageview signal", value: "500,000+", note: "a month; a signal, not the main rule", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Payment minimum", value: "$200", note: "in Publift's comparison table", sourceUrl: "https://www.publift.com/blog/the-best-cpm-ad-network-for-publishers" }], joining: [{ label: "Traffic", value: "500,000 pageviews is a secondary signal", note: "Monthly; revenue is the primary qualifying metric", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Visitor location", value: "No location-based limit stated", note: "Publift says it does not discriminate by traffic origin", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Content", value: "Useful, safe, original content", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Ad revenue", value: "More than $2,000 a month", note: "Primary qualifying metric, in USD", sourceUrl: "https://www.publift.com/blog/publift-requirements" }], minTraffic: null, revenueShare: null, paymentTerms: "Google revenue: Net 30; other network revenue: Net 90 in Publift's current comparison article", paymentThreshold: "$200 in Publift's published comparison table", paymentMethods: ["Direct bank transfer"], sources: [{ label: "2026 requirements", url: "https://www.publift.com/blog/publift-requirements" }, { label: "2026 publisher payment comparison", url: "https://www.publift.com/blog/ezoic-vs-adsense-vs-publift" }] },
+ publir: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pro fee", value: "15%", note: "Publir's fee on its Pro service; custom ads use a separate split", sourceUrl: "https://publir.com/app/pricing" }, { label: "Custom ad share", value: "50/50", note: "publisher and Publir, Net 60, in the service agreement", sourceUrl: "https://publir.com/service_agreement" }], joining: [{ label: "Traffic", value: "No traffic limit", sourceUrl: "https://publir.com/app/pricing" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60 for custom ad revenue under service agreement", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Pricing", url: "https://publir.com/app/pricing" }, { label: "Service agreement", url: "https://publir.com/service_agreement" }] },
+ "publisher-collective": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "€500", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }], joining: [{ label: "Traffic", value: "No numeric minimum stated", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Own or license the media; no prohibited content", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Integrate its systems and ad tags", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Paid after advertiser payment; no fixed interval is published in the partner terms", paymentThreshold: "€500", paymentMethods: ["Bank transfer; GBP, EUR, or USD"] , sources: [{ label: "Partner terms", url: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Publisher FAQ", url: "https://www.publisher-collective.com/faq" }, { label: "Google Ad Exchange article", url: "https://www.publisher-collective.com/blog/google-ad-exchange" }] },
+ pubnation: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Official website", url: "https://pubnation.com/" }] },
+ pubpower: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 0 or Net 30", note: "Net 0 above $3,000 a month in revenue", sourceUrl: "https://help.pubpower.io/article/payment-methods-terms" }, { label: "Payoneer minimum", value: "$50", sourceUrl: "https://help.pubpower.io/article/payment-methods-terms" }, { label: "Bank transfer minimum", value: "$1,000", sourceUrl: "https://help.pubpower.io/article/payment-methods-terms" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 0 above $3,000 monthly revenue; otherwise Net 30", paymentThreshold: "Method-specific: $50 Payoneer, $100 PayPal, $100 to $1,000 crypto, or $1,000 bank", paymentMethods: ["Bank transfer", "PayPal", "Payoneer", "Cryptocurrency"], sources: [{ label: "Payment methods and terms", url: "https://help.pubpower.io/article/payment-methods-terms" }] },
+ raptive: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "25,000", note: "monthly pageviews", sourceUrl: "https://raptive.com/creators/" }, { label: "Your share", value: "75%", sourceUrl: "https://raptive.com/fbp/" }, { label: "Pay day", value: "Net 45", sourceUrl: "https://help.raptive.com/hc/en-us/articles/202052255-When-do-I-get-paid" }], joining: [{ label: "Traffic", value: "At least 25,000 pageviews", note: "Monthly", sourceUrl: "https://raptive.com/creators/" }, { label: "Visitor location", value: "Majority from the US, Canada, UK, Australia, or New Zealand", sourceUrl: "https://raptive.com/creators/" }, { label: "Content", value: "100% original content", sourceUrl: "https://raptive.com/creators/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Set up Google Analytics", sourceUrl: "https://raptive.com/creators/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 25000, unit: "pageviews" }, revenueShare: "75%", paymentTerms: "Net 45; paid on the 15th", paymentThreshold: "$25; $100 for wire transfer", paymentMethods: ["ACH", "PayPal", "Wire transfer"], sources: [{ label: "Creator eligibility", url: "https://raptive.com/creators/" }, { label: "Share information", url: "https://raptive.com/fbp/" }, { label: "Payment FAQ", url: "https://help.raptive.com/hc/en-us/articles/202052255-When-do-I-get-paid" }] },
+ refinery89: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "€250 or $250", note: "a month, by region", sourceUrl: "https://refinery89.com/docs/faqs/finance/is-there-a-minimum-threshold-for-getting-paid/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: "€250 or $250, based on region", paymentMethods: [], sources: [{ label: "Payment threshold", url: "https://refinery89.com/docs/faqs/finance/is-there-a-minimum-threshold-for-getting-paid/" }, { label: "Payment timing", url: "https://refinery89.com/docs/faqs/finance/when-do-publishers-get-paid/" }, { label: "Publisher solution", url: "https://refinery89.com/publisher-ad-management-solution-refinery89/" }] },
+ revcontent: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "$250", note: "in the publisher terms; payment methods set their own too", sourceUrl: "https://help.revcontent.com/knowledge/publisher-terms" }], joining: [{ label: "Traffic", value: "No universal minimum stated", sourceUrl: "https://help.revcontent.com/knowledge/publisher-terms" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Comply with publisher terms and content policy", sourceUrl: "https://help.revcontent.com/knowledge/publisher-terms" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: "$250 in terms; payment method thresholds range from $100 ACH to $1,000 wire", paymentMethods: ["ACH", "PayPal", "Wire transfer"], sources: [{ label: "Publisher terms", url: "https://help.revcontent.com/knowledge/publisher-terms" }, { label: "Payment requirements and methods", url: "https://help.revcontent.com/knowledge/publisher-payment-requirements-methods" }] },
+ setupad: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "100,000", note: "monthly visitors or sessions; dated articles differ, so check current rules", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Payment minimum", value: "$100", note: "in published guidance", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }], joining: [{ label: "Traffic", value: "Not public", note: "Older official articles conflict on visitors versus sessions", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Follow publisher policy", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Maintain ads.txt", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60 in published guidance", paymentThreshold: "$100 in published guidance", paymentMethods: ["Bank transfer", "PayPal", "Payoneer", "Wise", "Revolut", "Paysera"], sources: [{ label: "Programmatic advertising guide", url: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Platform comparison", url: "https://setupad.com/es/blog/ezoic-vs-adsense-vs-setupad/" }] },
+ "she-media": { checkedAt: "2026-10-16", keyNumbers: [{ label: "PayPal cutoff", value: "$1,000", note: "a month, outside the US; wire is preferred above it", sourceUrl: "https://support.shemedia.com/hc/en-us/articles/35987097977627-Payment-Options-for-Publishers-Outside-the-US" }, { label: "Referral traffic", value: "20,000", note: "monthly pageviews, in referral guidance only; not a general minimum", sourceUrl: "https://www.shemedia.com/news-updates/she-media-publisher-referral-program" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Quality content", note: "The referral page describes referral eligibility, not a general applicant checklist", sourceUrl: "https://www.shemedia.com/news-updates/she-media-publisher-referral-program" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: ["PayPal", "WISE/ACH", "Wire transfer"], sources: [{ label: "International payment options", url: "https://support.shemedia.com/hc/en-us/articles/35987097977627-Payment-Options-for-Publishers-Outside-the-US" }, { label: "Publisher referral criteria", url: "https://www.shemedia.com/news-updates/she-media-publisher-referral-program" }] },
+ "snack-media": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Notice to leave", value: "30 days", sourceUrl: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No hateful, pornographic, illegal, or infringing content", sourceUrl: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Share live traffic stats and install network tags", note: "Tags must be live within 14 days of the agreement date", sourceUrl: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Within 30 days after the month when Snack Media receives the advertiser funds", paymentThreshold: null, paymentMethods: [], sources: [{ label: "2025 publisher agreement terms", url: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }, { label: "Terms page", url: "https://www.snack-media.com/Ad_Network_Terms_and_Conditions_for_Publishers/" }] },
+ sovrn: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 45", sourceUrl: "https://authcore.atlassian.net/wiki/spaces/KB/pages/2693627510846" }, { label: "Payment minimum", value: "$25", note: "$50 for wire", sourceUrl: "https://authcore.atlassian.net/wiki/spaces/KB/pages/2693627510846" }], joining: [{ label: "Traffic", value: "No minimum stated", sourceUrl: "https://knowledge.sovrn.com/kb/what-is-required-for-sovrn-to-approve-my-property" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Operational domain, ad-system ID, and ads.txt", sourceUrl: "https://knowledge.sovrn.com/kb/what-is-required-for-sovrn-to-approve-my-property" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 45 for Ad Exchange", paymentThreshold: "$25 standard; $50 for wire", paymentMethods: ["PayPal", "ACH", "Wire", "eCheck/local transfer", "US check"], sources: [{ label: "Account requirements", url: "https://knowledge.sovrn.com/kb/what-is-required-for-sovrn-to-approve-my-property" }, { label: "Payment help", url: "https://authcore.atlassian.net/wiki/spaces/KB/pages/2693627510846" }] },
+ sparteo: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Company and services", url: "https://corporate.sparteo.com/" }, { label: "Publisher platform case study", url: "https://corporate.sparteo.com/post/w3schools-rpm-uplift-unified-monetisation-stack" }] },
+ taboola: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Within 45 days", note: "after the month ends", sourceUrl: "https://www.taboola.com/policies/publisher-revshare-terms-and-conditions-1-3-US" }], joining: [{ label: "Traffic", value: "No numeric minimum stated", sourceUrl: "https://www.taboola.com/policies/publisher-revshare-terms-and-conditions-1-3-US" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Comply with the Publisher Policy", sourceUrl: "https://pubhelp.taboola.com/hc/en-us/articles/360033751274-Taboola-Publisher-Policy" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Within 45 days after month end", paymentThreshold: null, paymentMethods: ["Payoneer"], sources: [{ label: "Publisher revenue share terms", url: "https://www.taboola.com/policies/publisher-revshare-terms-and-conditions-1-3-US" }, { label: "Publisher policy", url: "https://pubhelp.taboola.com/hc/en-us/articles/360033751274-Taboola-Publisher-Policy" }] },
+ teads: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "30 to 60 days", note: "typical range in a company filing; contracts are not public", sourceUrl: "https://investors.teads.com/static-files/4f470000-7ee3-4919-9e4d-506e65a44af4" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Teads' company filing describes 30 to 60 days; exact agreement is not public", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Media owner partnership", url: "https://www.teads.com/media-owners" }, { label: "Company filing", url: "https://investors.teads.com/static-files/4f470000-7ee3-4919-9e4d-506e65a44af4" }] },
+ "the-moneytizer": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "€50 or $50", sourceUrl: "https://www.themoneytizer.com/payment-simulator" }, { label: "Pay day", value: "About two months", note: "after the month you earn it", sourceUrl: "https://www.themoneytizer.com/payment-simulator" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Invoice is generated after month end and payment follows about two months later", paymentThreshold: "€50 or $50", paymentMethods: [], sources: [{ label: "Payment simulator", url: "https://www.themoneytizer.com/payment-simulator" }, { label: "US adtech glossary", url: "https://us.themoneytizer.com/adtech-glossary?id=16" }] },
+ venatus: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Official publisher platform", url: "https://www.venatus.com/" }, { label: "Publisher interest form", url: "https://info.venatus.com/en/signup" }] },
+};
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/automotive/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/automotive/intro.mdx
new file mode 100644
index 00000000..0c21a465
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/automotive/intro.mdx
@@ -0,0 +1 @@
+The ad networks car and truck sites run, from repair guides to buying advice, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/beauty-fashion/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/beauty-fashion/intro.mdx
new file mode 100644
index 00000000..b03d71de
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/beauty-fashion/intro.mdx
@@ -0,0 +1 @@
+The ad networks beauty, skin care, and fashion sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/business/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/business/intro.mdx
new file mode 100644
index 00000000..c2444159
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/business/intro.mdx
@@ -0,0 +1 @@
+The ad networks small business, marketing, and career sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/crafts/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/crafts/intro.mdx
new file mode 100644
index 00000000..8c680432
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/crafts/intro.mdx
@@ -0,0 +1 @@
+The ad networks sewing, knitting, and craft sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/education/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/education/intro.mdx
new file mode 100644
index 00000000..3b9d7fc1
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/education/intro.mdx
@@ -0,0 +1 @@
+The ad networks learning, school help, and reference sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/entertainment/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/entertainment/intro.mdx
new file mode 100644
index 00000000..8bf6c3f1
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/entertainment/intro.mdx
@@ -0,0 +1 @@
+The ad networks TV, movie, music, and celebrity sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/food/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/food/intro.mdx
new file mode 100644
index 00000000..b62940b2
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/food/intro.mdx
@@ -0,0 +1 @@
+The ad networks food blogs and recipe sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/gaming/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gaming/intro.mdx
new file mode 100644
index 00000000..685fd93c
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gaming/intro.mdx
@@ -0,0 +1 @@
+The ad networks game guides, wikis, and gaming news sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/gardening/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gardening/intro.mdx
new file mode 100644
index 00000000..dd6531a8
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gardening/intro.mdx
@@ -0,0 +1 @@
+The ad networks gardening and yard care sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/health-fitness/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/health-fitness/intro.mdx
new file mode 100644
index 00000000..88f9fabe
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/health-fitness/intro.mdx
@@ -0,0 +1 @@
+The ad networks health, fitness, and wellness sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/home-diy/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/home-diy/intro.mdx
new file mode 100644
index 00000000..42db5434
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/home-diy/intro.mdx
@@ -0,0 +1 @@
+The ad networks home decor, cleaning, and DIY sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/index.ts b/app/(site)/(directories)/ad-networks/niches/[niche]/content/index.ts
new file mode 100644
index 00000000..1f6a0116
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/index.ts
@@ -0,0 +1,45 @@
+import type { MDXContent } from "mdx/types";
+import Intro1 from "./food/intro.mdx";
+import Intro2 from "./home-diy/intro.mdx";
+import Intro3 from "./travel/intro.mdx";
+import Intro4 from "./parenting/intro.mdx";
+import Intro5 from "./personal-finance/intro.mdx";
+import Intro6 from "./health-fitness/intro.mdx";
+import Intro7 from "./beauty-fashion/intro.mdx";
+import Intro8 from "./gaming/intro.mdx";
+import Intro9 from "./tech/intro.mdx";
+import Intro10 from "./crafts/intro.mdx";
+import Intro11 from "./pets/intro.mdx";
+import Intro12 from "./outdoors/intro.mdx";
+import Intro13 from "./gardening/intro.mdx";
+import Intro14 from "./sports/intro.mdx";
+import Intro15 from "./entertainment/intro.mdx";
+import Intro16 from "./lifestyle/intro.mdx";
+import Intro17 from "./education/intro.mdx";
+import Intro18 from "./automotive/intro.mdx";
+import Intro19 from "./business/intro.mdx";
+import Intro20 from "./news/intro.mdx";
+
+/** Words for each niche page, keyed by niche slug. */
+export const nicheContent: Record = {
+ "food": { Intro: Intro1 },
+ "home-diy": { Intro: Intro2 },
+ "travel": { Intro: Intro3 },
+ "parenting": { Intro: Intro4 },
+ "personal-finance": { Intro: Intro5 },
+ "health-fitness": { Intro: Intro6 },
+ "beauty-fashion": { Intro: Intro7 },
+ "gaming": { Intro: Intro8 },
+ "tech": { Intro: Intro9 },
+ "crafts": { Intro: Intro10 },
+ "pets": { Intro: Intro11 },
+ "outdoors": { Intro: Intro12 },
+ "gardening": { Intro: Intro13 },
+ "sports": { Intro: Intro14 },
+ "entertainment": { Intro: Intro15 },
+ "lifestyle": { Intro: Intro16 },
+ "education": { Intro: Intro17 },
+ "automotive": { Intro: Intro18 },
+ "business": { Intro: Intro19 },
+ "news": { Intro: Intro20 },
+};
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/lifestyle/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/lifestyle/intro.mdx
new file mode 100644
index 00000000..f9c4432f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/lifestyle/intro.mdx
@@ -0,0 +1 @@
+The ad networks lifestyle and personal blogs run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/news/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/news/intro.mdx
new file mode 100644
index 00000000..71180fb8
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/news/intro.mdx
@@ -0,0 +1 @@
+The ad networks news and current events sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/outdoors/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/outdoors/intro.mdx
new file mode 100644
index 00000000..b8b3316b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/outdoors/intro.mdx
@@ -0,0 +1 @@
+The ad networks camping, hiking, hunting, and fishing sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/parenting/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/parenting/intro.mdx
new file mode 100644
index 00000000..eccea590
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/parenting/intro.mdx
@@ -0,0 +1 @@
+The ad networks parenting, pregnancy, and family sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/personal-finance/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/personal-finance/intro.mdx
new file mode 100644
index 00000000..a56aa21f
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/personal-finance/intro.mdx
@@ -0,0 +1 @@
+The ad networks personal finance sites run, from budgeting blogs to investing guides, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/pets/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/pets/intro.mdx
new file mode 100644
index 00000000..3ced3d20
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/pets/intro.mdx
@@ -0,0 +1 @@
+The ad networks dog, cat, and pet care sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/sports/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/sports/intro.mdx
new file mode 100644
index 00000000..86bd49ec
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/sports/intro.mdx
@@ -0,0 +1 @@
+The ad networks sports, scores, and fantasy sports sites run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/tech/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/tech/intro.mdx
new file mode 100644
index 00000000..2e6ce0b5
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/tech/intro.mdx
@@ -0,0 +1 @@
+The ad networks tech sites run, from how-to guides to device reviews, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/travel/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/travel/intro.mdx
new file mode 100644
index 00000000..66c8d4e6
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/travel/intro.mdx
@@ -0,0 +1 @@
+The ad networks travel blogs and destination guides run, and the sites that run them.
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/layout.tsx b/app/(site)/(directories)/ad-networks/niches/[niche]/layout.tsx
new file mode 100644
index 00000000..237d950d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/layout.tsx
@@ -0,0 +1,7 @@
+import { registryLayout } from "@/lib/registry-layout";
+
+const registry = registryLayout("/ad-networks/niches/[niche]");
+export const generateMetadata = registry.generateMetadata;
+export function generateStaticParams() { return []; }
+export const revalidate = false;
+export default registry.Layout;
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/niche-view.tsx b/app/(site)/(directories)/ad-networks/niches/[niche]/niche-view.tsx
new file mode 100644
index 00000000..67fb8529
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/niche-view.tsx
@@ -0,0 +1,159 @@
+import Link from "next/link";
+import type { ReactNode } from "react";
+import type { MDXContent } from "mdx/types";
+import { Shapes } from "lucide-react";
+import type { Breadcrumb } from "@/lib/breadcrumbs";
+import type { AdNetworkNichePageData } from "@/lib/ad-networks-types";
+import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content";
+import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon";
+import { NetworkLadder } from "@/app/ui/ad-networks/network-ladder";
+import { AdSection } from "@/app/ui/ad-networks/section";
+import { checkerHref, formatDay } from "@/app/ui/ad-networks/site-format";
+import { SitesTable } from "@/app/ui/ad-networks/sites-table";
+import { MoveBars } from "@/app/ui/ad-networks/viz/bars";
+import { NewestList } from "@/app/ui/ad-networks/viz/lists";
+import { SizeStacks } from "@/app/ui/ad-networks/viz/size-stacks";
+import { ReportBlock, ReportRow } from "@/app/ui/report";
+import { AppShell } from "@/app/ui/site/app-shell";
+import { EntryHero } from "@/app/ui/site/entry-hero";
+import { PageCanvas } from "@/app/ui/site/page-canvas";
+import { PageHeader } from "@/app/ui/site/page-header";
+import { PageMeta } from "@/app/ui/site/page-meta";
+import { Figure, NOT_PUBLIC, adNetworksSectionNav, joinLabel, joinOrder } from "../../ad-networks-shared";
+import { trafficLabel, trafficOrder } from "@/lib/ad-networks-report";
+import styles from "../niches.module.css";
+
+/** Networks drawn by name in the size stacks. The rest are grouped as Other. */
+const sizeNetworks = 5;
+
+const inlineIntro = { p: ({ children }: { children?: ReactNode }) => <>{children}> };
+/** Short names from the mockup, such as `AdSense` and `Journey`, where space is tight. */
+const shortName = (name: string) => name.replace(/^Google /, "").replace(/ by Mediavine$/, "");
+const networkHref = (slug: string) => `/ad-networks/${slug}`;
+
+/** Each size group as 100% split across the niche's biggest networks, with the rest as Other. */
+function sizeStacks(traffic: AdNetworkNichePageData["traffic"]) {
+ const totals = new Map();
+ for (const group of traffic) {
+ for (const network of group.networks) {
+ totals.set(network.slug, { name: network.name, count: (totals.get(network.slug)?.count ?? 0) + network.count });
+ }
+ }
+ const shown = [...totals.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, sizeNetworks);
+ const groups = [...traffic].sort((a, b) => trafficOrder(a.traffic_group) - trafficOrder(b.traffic_group)).map((group) => {
+ // Raw shares from counts; Other is the sites left over, so rounding never invents a slice.
+ const counts = shown.map(([slug]) => group.networks.find((row) => row.slug === slug)?.count ?? 0);
+ const rest = Math.max(0, group.sites - counts.reduce((sum, value) => sum + value, 0));
+ const share = (count: number) => (group.sites ? (count / group.sites) * 100 : 0);
+ return { label: trafficLabel(group.traffic_group), sites: group.sites, shares: [...counts.map(share), share(rest)] };
+ });
+ return { groups, series: [...shown.map(([, network]) => ({ label: network.name })), { label: "Other" }] };
+}
+
+/**
+ * One niche (mockup `food.html`): its sites, the networks they run with what it takes to join,
+ * what sites of each size run, and recent moves. Takes loader data so it can be checked with any data.
+ */
+export function NicheView({ breadcrumbs, data, Intro, title, updatedAt }: {
+ breadcrumbs: Breadcrumb[];
+ data: AdNetworkNichePageData;
+ Intro: MDXContent;
+ title: string;
+ updatedAt: string | Date | null;
+}) {
+ const { niche, sites, switches } = data;
+ const noun = `${niche.name.toLowerCase()} sites`;
+ const leader = niche.networks[0];
+ const sizes = sizeStacks(data.traffic);
+ const newest = (data.newest_arrivals ?? []).map((arrival) => ({
+ domain: arrival.site.domain,
+ href: checkerHref(arrival.site.domain),
+ network: shortName(arrival.network.name),
+ networkHref: networkHref(arrival.network.slug),
+ date: formatDay(arrival.observed_at) ?? "",
+ }));
+ const moves = switches.slice(0, 6).map((row) => ({
+ from: shortName(row.from.name),
+ fromHref: networkHref(row.from.slug),
+ to: shortName(row.to.name),
+ toHref: networkHref(row.to.slug),
+ count: row.count,
+ }));
+ const movesDescription = moves.length && newest.length
+ ? `${niche.name} sites that switched networks in the last 90 days, and the newest sites on each network.`
+ : moves.length ? `${niche.name} sites that switched networks in the last 90 days.` : `The newest ${noun} we found, and the network each one joined.`;
+
+ return (
+
+
+
+
} iconKey={null} />
+
+
+ We list {niche.listed} {noun}, and {niche.confirmed} {niche.confirmed === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code.
+ {leader ? <> {leader.name} runs on more of them than any other network.> : null}
+
+
+
+
+ ({ name, slug }))}
+ noun={noun}
+ route={`/ad-networks/niches/${niche.slug}`}
+ scope={{ niche: niche.slug }}
+ />
+
+ {niche.networks.length ? (
+ {
+ const rule = joinLabel(network.slug).value;
+ return {
+ name: shortName(network.name),
+ href: networkHref(network.slug),
+ sites: network.count,
+ rule: rule === NOT_PUBLIC || rule === "Not listed" ? null : rule,
+ ruleRank: joinOrder(network.slug),
+ };
+ })}
+ shareLabel={`share of ${noun}`}
+ title={`Which ad networks do ${noun} use?`}
+ total={niche.listed}
+ />
+ ) : null}
+
+ {/* data-gate: the traffic run and resolved main managers fill the size groups. */}
+ {sizes.groups.length ? (
+
+
+
+ ) : null}
+
+ {/* data-gate: the verifier's manager replacements fill switches; observed arrivals (never the starting import) fill newest sites. */}
+ {moves.length || newest.length ? (
+
+
+ {moves.length ? (
+
+
+
+ ) : null}
+ {newest.length ? (
+
+
+
+ ) : null}
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/page.tsx b/app/(site)/(directories)/ad-networks/niches/[niche]/page.tsx
new file mode 100644
index 00000000..6362a85b
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/[niche]/page.tsx
@@ -0,0 +1,27 @@
+import { notFound } from "next/navigation";
+import { adNetworkNiches } from "@/lib/ad-network-niches";
+import { getBreadcrumbs } from "@/lib/breadcrumbs";
+import { getRequiredDirectoryPage } from "@/lib/directory-pages";
+import { getAdNetworkNichePageData } from "@/lib/ad-networks";
+import { nicheContent } from "./content";
+import { NicheView } from "./niche-view";
+
+// Build every niche during the release. Unpublished niches still return 404.
+export function generateStaticParams() {
+ return adNetworkNiches.map((niche) => ({ niche: niche.slug }));
+}
+
+export default async function AdNetworkNichePage({ params }: { params: Promise<{ niche: string }> }) {
+ const { niche: slug } = await params;
+ const content = nicheContent[slug];
+ if (!content) throw new Error(`Ad network niche has no MDX content: ${slug}`);
+ const directoryPage = await getRequiredDirectoryPage(`ad-networks/niches/${slug}`);
+ const [data, breadcrumbs] = await Promise.all([
+ getAdNetworkNichePageData(slug),
+ getBreadcrumbs(`ad-networks/niches/${slug}`),
+ ]);
+ // data-gate: the niche run sorts sites into this niche; until it reaches coverage the loader returns null.
+ if (!data) notFound();
+
+ return ;
+}
diff --git a/app/(site)/(directories)/ad-networks/niches/content/article.mdx b/app/(site)/(directories)/ad-networks/niches/content/article.mdx
new file mode 100644
index 00000000..0d5b8d6d
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/content/article.mdx
@@ -0,0 +1,7 @@
+## How do we sort sites into niches?
+
+Niches are estimates based on each site's homepage text. A site that covers several topics goes into its main one. Low confidence guesses stay unsorted and don't count toward any niche here.
+
+## Why can a niche's shares add up to more than 100%?
+
+Many sites run more than one ad network at the same time. Each share counts the sites that use that network, so one site can count toward several networks in the same niche.
diff --git a/app/(site)/(directories)/ad-networks/niches/content/intro.mdx b/app/(site)/(directories)/ad-networks/niches/content/intro.mdx
new file mode 100644
index 00000000..2022af21
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/content/intro.mdx
@@ -0,0 +1 @@
+See which ad networks sites use in each niche, from food and travel to gaming.
diff --git a/app/(site)/(directories)/ad-networks/niches/layout.tsx b/app/(site)/(directories)/ad-networks/niches/layout.tsx
new file mode 100644
index 00000000..0ac770de
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/layout.tsx
@@ -0,0 +1,6 @@
+import { registryLayout } from "@/lib/registry-layout";
+
+const registry = registryLayout("/ad-networks/niches", { structuredData: "page" });
+export const generateMetadata = registry.generateMetadata;
+export const revalidate = false;
+export default registry.Layout;
diff --git a/app/(site)/(directories)/ad-networks/niches/niches-view.tsx b/app/(site)/(directories)/ad-networks/niches/niches-view.tsx
new file mode 100644
index 00000000..7d7aae16
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/niches/niches-view.tsx
@@ -0,0 +1,112 @@
+import type { ReactNode } from "react";
+import { Shapes } from "lucide-react";
+import type { Breadcrumb } from "@/lib/breadcrumbs";
+import type { AdNetworkNichesPageData } from "@/lib/ad-networks-types";
+import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content";
+import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon";
+import { NichesTable, type NicheTableRow } from "@/app/ui/ad-networks/niches-table";
+import { AdSection } from "@/app/ui/ad-networks/section";
+import { Heatmap } from "@/app/ui/ad-networks/viz/heatmap";
+import { Treemap } from "@/app/ui/ad-networks/viz/treemap";
+import { AppShell } from "@/app/ui/site/app-shell";
+import { EntryHero } from "@/app/ui/site/entry-hero";
+import { PageCanvas } from "@/app/ui/site/page-canvas";
+import { PageHeader } from "@/app/ui/site/page-header";
+import { PageMeta } from "@/app/ui/site/page-meta";
+import { proseClassName, proseFlowClassName } from "@/app/ui/site/prose";
+import { Figure, adNetworksSectionNav, joinLabel } from "../ad-networks-shared";
+import Article from "./content/article.mdx";
+import Intro from "./content/intro.mdx";
+import styles from "./niches.module.css";
+
+/** Networks shown as columns in the niche grid: the ones on the most sites. */
+const gridNetworks = 9;
+
+const inlineIntro = { p: ({ children }: { children?: ReactNode }) => <>{children}> };
+/** Short names from the mockup, such as `AdSense` and `Journey`, where space is tight. */
+const shortName = (name: string) => name.replace(/^Google /, "").replace(/ by Mediavine$/, "");
+const networkHref = (slug: string) => `/ad-networks/${slug}`;
+
+/**
+ * The niches index (mockup `niches.html`): sites per niche, each niche's top networks, and the
+ * niche by network grid. Takes loader data so it can be checked with any data.
+ */
+export function NichesView({ breadcrumbs, data, nicheHref, title, updatedAt }: {
+ breadcrumbs: Breadcrumb[];
+ data: AdNetworkNichesPageData;
+ /** The niche page link, or undefined while that page isn't published. */
+ nicheHref: (slug: string) => string | undefined;
+ title: string;
+ updatedAt: string | Date | null;
+}) {
+ const { niches } = data;
+ const rows: NicheTableRow[] = niches.map((niche) => ({
+ slug: niche.slug,
+ name: niche.name,
+ href: nicheHref(niche.slug),
+ sites: niche.listed,
+ live: niche.confirmed,
+ share: niche.share / 100,
+ top: niche.networks.slice(0, 3).map((network) => ({ name: shortName(network.name), href: networkHref(network.slug), share: network.share / 100 })),
+ easiest: niche.easiest_to_join
+ ? { name: shortName(niche.easiest_to_join.name), href: networkHref(niche.easiest_to_join.slug), rule: joinLabel(niche.easiest_to_join.slug).value }
+ : undefined,
+ networkShares: Object.fromEntries(niche.networks.map((network) => [network.slug, network.share / 100])),
+ }));
+ const columns = data.networks.slice(0, gridNetworks);
+
+ return (
+
+
+
+
} iconKey={null} />
+
+
+
+ {niches.length ? (
+ <> We sort {data.classified} of the {data.listed} sites we list into {niches.length} niches.{data.unclassified ? <> The other {data.unclassified} aren't sorted yet.> : null}>
+ ) : null}
+
+ A directory of {data.networks_tracked} ad networks for publishers, from Ezoic and Mediavine to Google AdSense. See what each one takes to join and how many sites run it.
+ {/* data-gate: the sync fills the listed and confirmed counts. */}
+ {data.listed ? <> We list {data.listed} sites, and {data.confirmed} {data.confirmed === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code.> : null}
+
+
+
+
+
+
+
+
+
-
- ;
+
+
+ >
+ );
}
diff --git a/app/(site)/(directories)/ad-networks/stats/content/article.mdx b/app/(site)/(directories)/ad-networks/stats/content/article.mdx
new file mode 100644
index 00000000..0ce41ced
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/stats/content/article.mdx
@@ -0,0 +1,3 @@
+## How do we count a site for a network?
+
+A site counts for a network when its own ads.txt file, or the ad setup on its pages, shows the network. A network's seller list can point us to a site, but it never counts one on its own. We re-check sites on a schedule, so these numbers move as sites join and leave.
diff --git a/app/(site)/(directories)/ad-networks/stats/content/intro.mdx b/app/(site)/(directories)/ad-networks/stats/content/intro.mdx
new file mode 100644
index 00000000..b823dcb4
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/stats/content/intro.mdx
@@ -0,0 +1 @@
+How many sites each ad network runs on, which networks are growing, and what the sites look like.
diff --git a/app/(site)/(directories)/ad-networks/stats/layout.tsx b/app/(site)/(directories)/ad-networks/stats/layout.tsx
new file mode 100644
index 00000000..a2bb4d41
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/stats/layout.tsx
@@ -0,0 +1,6 @@
+import { registryLayout } from "@/lib/registry-layout";
+
+const registry = registryLayout("/ad-networks/stats");
+export const generateMetadata = registry.generateMetadata;
+export const revalidate = false;
+export default registry.Layout;
diff --git a/app/(site)/(directories)/ad-networks/stats/page.tsx b/app/(site)/(directories)/ad-networks/stats/page.tsx
new file mode 100644
index 00000000..0faa69cf
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/stats/page.tsx
@@ -0,0 +1,304 @@
+import type { ReactNode } from "react";
+import { ChartColumn } from "lucide-react";
+import { getBreadcrumbs } from "@/lib/breadcrumbs";
+import { getRequiredDirectoryPage } from "@/lib/directory-pages";
+import { getAdNetworkStatsPageData } from "@/lib/ad-networks";
+import type { AdNetworkStatsPageData } from "@/lib/ad-networks-types";
+import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content";
+import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon";
+import { Report, ReportBars, ReportBlock, ReportGroup, ReportNumbers, ReportRow, type ReportNumber } from "@/app/ui/report";
+import { ColumnChart } from "@/app/ui/ad-networks/viz/columns";
+import { DivergingBars } from "@/app/ui/ad-networks/viz/diverging-bars";
+import { DotPlot } from "@/app/ui/ad-networks/viz/dot-plot";
+import { formatCompact, logStepPosition } from "@/app/ui/ad-networks/viz/format";
+import { MultiLine, type LineSeries } from "@/app/ui/ad-networks/viz/multi-line";
+import { ScatterPlot } from "@/app/ui/ad-networks/viz/scatter-plot";
+import { SizeStacks } from "@/app/ui/ad-networks/viz/size-stacks";
+import { StackedColumns } from "@/app/ui/ad-networks/viz/stacked-columns";
+import { formatCount } from "@/app/ui/ad-networks/site-format";
+import { AppShell } from "@/app/ui/site/app-shell";
+import { EntryHero } from "@/app/ui/site/entry-hero";
+import { PageCanvas } from "@/app/ui/site/page-canvas";
+import { PageHeader } from "@/app/ui/site/page-header";
+import { PageMeta } from "@/app/ui/site/page-meta";
+import { proseClassName, proseFlowClassName } from "@/app/ui/site/prose";
+import { Figure, adNetworksSectionNav, percent } from "../ad-networks-shared";
+import { trafficLabel, trafficOrder } from "@/lib/ad-networks-report";
+import Article from "./content/article.mdx";
+import Intro from "./content/intro.mdx";
+import styles from "./stats.module.css";
+
+/** Networks drawn by name in the size stacks. The rest are grouped as Other. */
+const sizeNetworks = 5;
+/** Line colors for the five biggest networks, in rank order, as in the mockup. */
+const lineTones: Pick[] = [{ tone: "ac" }, { tone: "ac2" }, { tone: "n1" }, { tone: "ac", dashed: true }, { tone: "n2" }];
+
+/** Chart labels use the short names from the mockup, such as `AdSense` and `Journey`. */
+const shortName = (name: string) => name.replace(/^Google /, "").replace(/ by Mediavine$/, "");
+const inlineIntro = { p: ({ children }: { children?: ReactNode }) => <>{children}> };
+const monthShort = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" });
+const monthFull = new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric", timeZone: "UTC" });
+const weekLabel = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
+const day = (iso: string) => Date.parse(`${iso.slice(0, 10)}T00:00:00Z`);
+
+/** How we confirmed each link, in words. Keys come from the verifier. */
+const methodLabels: Record = {
+ ads_txt_managerdomain: "ads.txt MANAGERDOMAIN line",
+ ads_txt_and_adsense_account: "ads.txt and AdSense account",
+ ads_txt_and_page_signature: "ads.txt and page ad setup",
+ page_signature: "Page ad setup only",
+};
+const methodLabel = (key: string) => methodLabels[key] ?? key.replace(/_/g, " ");
+
+/** Month-end counts of the biggest networks, on the dates every line has. */
+function shareOverTime(history: AdNetworkStatsPageData["history"]) {
+ const dates = [...new Set(history.flatMap((network) => network.history.map((row) => row.date)))]
+ .filter((date) => history.every((network) => network.history.some((row) => row.date === date)))
+ .sort();
+ return {
+ points: dates.map((date) => ({ label: monthShort.format(day(date)), fullLabel: monthFull.format(day(date)) })),
+ series: history.slice(0, lineTones.length).map((network, index) => ({
+ label: network.name,
+ shortLabel: shortName(network.name),
+ values: dates.map((date) => network.history.find((row) => row.date === date)?.listed ?? 0),
+ ...lineTones[index],
+ })),
+ };
+}
+
+/** Each size group as 100% split across the biggest networks, with the rest as Other. */
+function sizeStacks(traffic: AdNetworkStatsPageData["traffic"]) {
+ const totals = new Map();
+ for (const group of traffic) {
+ for (const network of group.networks) {
+ totals.set(network.slug, { name: network.name, count: (totals.get(network.slug)?.count ?? 0) + network.count });
+ }
+ }
+ const shown = [...totals.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, sizeNetworks);
+ const groups = [...traffic].sort((a, b) => trafficOrder(a.traffic_group) - trafficOrder(b.traffic_group)).map((group) => {
+ // Raw shares from counts; Other is the sites left over, so rounding never invents a slice.
+ const counts = shown.map(([slug]) => group.networks.find((row) => row.slug === slug)?.count ?? 0);
+ const rest = Math.max(0, group.sites - counts.reduce((sum, value) => sum + value, 0));
+ const share = (count: number) => (group.sites ? (count / group.sites) * 100 : 0);
+ return { label: trafficLabel(group.traffic_group), sites: group.sites, shares: [...counts.map(share), share(rest)] };
+ });
+ return { groups, series: [...shown.map(([, network]) => ({ label: network.name })), { label: "Other" }] };
+}
+
+
+// Monthly traffic minimums on the joining rule chart's x axis (mockup `SCX`).
+const ruleSteps = [0, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000];
+const ruleStepLabels = ruleSteps.map((step) => (step ? formatCompact(step) : "none"));
+export default async function AdNetworkStatsPage() {
+ const directoryPage = await getRequiredDirectoryPage("ad-networks/stats");
+ const [data, breadcrumbs] = await Promise.all([
+ getAdNetworkStatsPageData(),
+ getBreadcrumbs("ad-networks/stats"),
+ ]);
+
+ const summary: ReportNumber[] = [
+ { label: "sites listed", value: formatCount(data.listed) },
+ { label: "confirmed live", value: formatCount(data.confirmed), accent: true },
+ { label: "networks tracked", value: formatCount(data.networks_tracked) },
+ ...(data.multiple_networks_share === null ? [] : [{ label: "run 2+ networks", value: percent(data.multiple_networks_share) }]),
+ ];
+ const shareRows = data.market_share.filter((row) => row.count > 0).slice(0, 12)
+ .map((row) => ({ label: row.name, value: row.count, display: `${row.share.toFixed(1)}% · ${formatCount(row.count)}`, href: `/ad-networks/${row.slug}` }));
+ const lines = shareOverTime(data.history);
+
+ const changes = [...data.net_change].sort((a, b) => b.change - a.change);
+ const changeRows = (changes.length > 10 ? [...changes.slice(0, 5), ...changes.slice(-5)] : changes)
+ .map((row) => ({ label: row.name, shortLabel: shortName(row.name), value: row.change }));
+ const switchRows = data.switches.slice(0, 6)
+ .map((row) => ({ label: `${shortName(row.from.name)} → ${shortName(row.to.name)}`, value: row.count, display: formatCount(row.count) }));
+
+ const sizes = sizeStacks(data.traffic);
+
+ const perSiteTotal = data.networks_per_site.reduce((sum, row) => sum + row.count, 0);
+ const perSite = data.networks_per_site.map((row, index) => {
+ const share = perSiteTotal ? (row.count / perSiteTotal) * 100 : 0;
+ return { label: row.key === "1" ? "1 network" : row.label, value: share, display: percent(share), tone: index ? "ac3" as const : "ac" as const };
+ });
+ const pairRows = data.pairs.slice(0, 5)
+ .map((row) => ({ label: `${shortName(row.first.name)} + ${shortName(row.second.name)}`, value: row.count, display: formatCount(row.count) }));
+
+ // The mockup's fixed steps. Each network sits by its real minimum on a log scale between them.
+ const scatter = data.joining_rules.map((rule) => ({
+ label: rule.name,
+ shortLabel: shortName(rule.name),
+ x: logStepPosition(rule.amount, ruleSteps),
+ xDisplay: rule.amount ? `${formatCount(rule.amount)} ${rule.unit}` : "none",
+ y: rule.sites,
+ }));
+ const ageRows = data.median_domain_age.slice(0, 9).map((row) => ({ label: row.name, shortLabel: shortName(row.name), value: Math.round(row.years) }));
+ const wordpressRows = data.wordpress_share.slice(0, 8)
+ .map((row) => ({ label: row.name, value: row.share, display: percent(row.share), href: `/ad-networks/${row.slug}` }));
+
+ const ads = data.ads_txt_metrics;
+ const fileSizeTop = Math.max(0, ...data.ads_txt.file_sizes.map((row) => row.count));
+ const fileSizes = data.ads_txt.file_sizes.map((row) => {
+ const share = data.ads_txt.measured ? (row.count / data.ads_txt.measured) * 100 : 0;
+ return { label: row.label, value: row.count, display: percent(share), tone: row.count === fileSizeTop ? "ac" as const : "n2" as const };
+ });
+
+ const fresh = data.freshness;
+ const freshNumbers: ReportNumber[] = [
+ ...(fresh.checked_share === null ? [] : [{ label: "checked in the last 7 days", value: percent(fresh.checked_share) }]),
+ ...(data.confirmed_share === null ? [] : [{ label: "confirmed live", value: `${data.confirmed_share.toFixed(1)}%`, accent: true }]),
+ ...(fresh.new_last_week === null ? [] : [{ label: "new sites last week", value: formatCount(fresh.new_last_week) }]),
+ ];
+ const weeks = fresh.weekly.map((row) => ({ label: `Week of ${weekLabel.format(day(row.week))}`, total: row.discovered + row.checker, part: row.checker }));
+ const methodTotal = data.verification_methods.reduce((sum, row) => sum + row.count, 0);
+ const methodRows = data.verification_methods.map((row) => ({
+ label: methodLabel(row.key),
+ value: row.count,
+ display: `${percent(methodTotal ? (row.count / methodTotal) * 100 : 0)} · ${formatCount(row.count)}`,
+ }));
+
+ return (
+
+
+
+
} iconKey={null} />
+
+
+ From our checks of {data.listed} sites across {data.networks_tracked} networks, with {data.confirmed} confirmed live.
+
+
+
+
+
+
+
+ {shareRows.length || lines.points.length > 1 ? (
+
+ {shareRows.length ? (
+
+
+
+ ) : null}
+ {/* data-gate: daily snapshots fill a second month end for the five biggest networks. */}
+ {lines.points.length > 1 ? (
+
+
+
+ ) : null}
+
+ ) : null}
+
+
+ {/* data-gate: 90 days of snapshots fill net change; the verifier's manager replacements fill switches. */}
+ {changeRows.length || switchRows.length ? (
+
+
+ {changeRows.length ? (
+
+
+
+ ) : null}
+ {switchRows.length ? (
+
+
+
+ ) : null}
+
+
+ ) : null}
+
+ {/* data-gate: the traffic run and resolved main managers fill the size groups. */}
+ {sizes.groups.length ? (
+
+
+
+
+
+ ) : null}
+
+ {perSiteTotal || pairRows.length ? (
+
+
+ {perSiteTotal ? (
+
+
+
+ ) : null}
+ {pairRows.length ? (
+
+
+
+ ) : null}
+
+
+ ) : null}
+
+ {/* data-gate: the domain-age run fills median ages; the platform run fills WordPress shares. */}
+ {scatter.length > 1 || ageRows.length || wordpressRows.length ? (
+
+
+ {scatter.length > 1 ? (
+
+
+
+ ) : null}
+ {ageRows.length ? (
+
+
+
+ ) : null}
+ {wordpressRows.length ? (
+
+
+
+ ) : null}
+
+
+ ) : null}
+
+ {/* data-gate: the ads-txt-summary run fills these numbers and the file sizes. */}
+ {ads ? (
+
+
+
+
+
+ {fileSizes.length ? (
+
+
+
+ ) : null}
+
+
+ ) : null}
+
+ {freshNumbers.length || weeks.length || methodRows.length ? (
+
+ {freshNumbers.length || weeks.length ? (
+
+ {freshNumbers.length ? : null}
+ {/* data-gate: real new-site observations with a source for each site fill the weekly columns. */}
+ {weeks.length ? (
+
+
+
+ ) : null}
+
+ ) : null}
+ {methodRows.length ? (
+
+
+
+ ) : null}
+
+ ) : null}
+
+
+
+
+
+
+ );
+}
diff --git a/app/(site)/(directories)/ad-networks/stats/stats.module.css b/app/(site)/(directories)/ad-networks/stats/stats.module.css
new file mode 100644
index 00000000..2a465203
--- /dev/null
+++ b/app/(site)/(directories)/ad-networks/stats/stats.module.css
@@ -0,0 +1,5 @@
+/* The method questions below the report (mockup `.nprose`). */
+.article {
+ margin-top: 64px;
+ max-width: var(--measure-prose);
+}
diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker-from-url.tsx b/app/(site)/(tools)/ad-network-checker/ad-network-checker-from-url.tsx
new file mode 100644
index 00000000..38fa7b0c
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker-from-url.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+import { useInitialQuery } from "@/app/ui/tools/use-initial-query";
+import { AdNetworkCheckerTool, type NetworkLogos } from "./ad-network-checker-tool";
+
+// Reads ?site= in the browser so the page stays static. A shared link checks that site right away.
+export function AdNetworkCheckerFromUrl({ logos }: { logos: NetworkLogos }) {
+ const query = useInitialQuery();
+ return ;
+}
diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker-tool.tsx b/app/(site)/(tools)/ad-network-checker/ad-network-checker-tool.tsx
new file mode 100644
index 00000000..a6c78236
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker-tool.tsx
@@ -0,0 +1,536 @@
+"use client";
+
+import Link from "next/link";
+import { CalendarDays, ExternalLink, RotateCcw } from "lucide-react";
+import { useEffect, useRef, useState, type FormEvent, type MouseEvent, type ReactNode, type RefObject } from "react";
+import { AdSection, SectionNote } from "@/app/ui/ad-networks/section";
+import { NetworkLogo } from "@/app/ui/ad-networks/network-logo";
+import { checkerHref, domainAge, nicheName } from "@/app/ui/ad-networks/site-format";
+import {
+ AdsTxtNumbers,
+ AdsTxtPreview,
+ BlockSubheading,
+ FactMuted,
+ SimilarSites,
+ SiteFacts,
+ SiteNetworksTable,
+ type SiteFact,
+} from "@/app/ui/ad-networks/site-view";
+import { TrafficMeter } from "@/app/ui/ad-networks/traffic-meter";
+import { RankedBars, StackBar } from "@/app/ui/ad-networks/viz/bars";
+import { formatNumber, formatShare, type VizTone } from "@/app/ui/ad-networks/viz/format";
+import { HistoryGantt, type GanttRow } from "@/app/ui/ad-networks/viz/history-gantt";
+import { EventsList } from "@/app/ui/ad-networks/viz/lists";
+import { UiButton, UiShareAction, UiTextField } from "@/app/ui/controls";
+import { UiActionRow, UiStatus } from "@/app/ui/display";
+import { ToolbarSearch } from "@/app/ui/table-toolbar";
+import { ToolResult, ToolWorkbench } from "@/app/ui/tools/tool-workbench";
+import type { AdNetworkCheckerSiteView, AdNetworkRef, AdNetworkSiteResponse } from "@/lib/ad-networks-types";
+import {
+ EXAMPLE_SITE,
+ adsTxtUrl,
+ eventsFor,
+ evidenceOf,
+ firstCheck,
+ formatDay,
+ formatMonth,
+ hasChanges,
+ isManager,
+ leadFor,
+ monthKey,
+ names,
+ normalizeSite,
+ saysLabel,
+ similarNetworks,
+ sortedRelationships,
+ statusLabel,
+} from "./ad-network-checker-view";
+import styles from "./ad-network-checker.module.css";
+
+/** Network slug to its logo path, read from `public/ad-networks/logos` by the server page. */
+export type NetworkLogos = Readonly>;
+
+type Outcome =
+ | { kind: "found"; view: AdNetworkCheckerSiteView }
+ | { domain: string; kind: "missing" }
+ | { domain: string; kind: "error"; message: string };
+
+class FieldError extends Error {}
+
+async function fetchSite(domain: string, signal: AbortSignal): Promise {
+ const response = await fetch(`/api/ad-networks/site?domain=${encodeURIComponent(domain)}`, { signal });
+ const data = await response.json().catch(() => null);
+ if (response.status === 400) throw new FieldError(typeof data?.error === "string" ? data.error : "Enter a domain such as example.com.");
+ if (!response.ok || !data) throw new Error("The check didn't finish. Try again in a moment.");
+ return data as AdNetworkSiteResponse;
+}
+
+export function AdNetworkCheckerTool({ initialSite = "", logos = {} }: { initialSite?: string; logos?: NetworkLogos }) {
+ const [site, setSite] = useState(initialSite || EXAMPLE_SITE);
+ const [error, setError] = useState(null);
+ const [outcome, setOutcome] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [edited, setEdited] = useState(false);
+ const inputRef = useRef(null);
+ const titleRef = useRef(null);
+ const resultRef = useRef(null);
+ const request = useRef(null);
+ const focusTitle = useRef(false);
+
+ async function check(raw: string, focus = true) {
+ const input = normalizeSite(raw);
+ if (!input.ok) {
+ setError(input.message);
+ requestAnimationFrame(() => inputRef.current?.focus());
+ return;
+ }
+ request.current?.abort();
+ const controller = new AbortController();
+ request.current = controller;
+ setError(null);
+ setEdited(false);
+ setLoading(true);
+ try {
+ const data = await fetchSite(input.domain, controller.signal);
+ setOutcome(data.found && data.site ? { kind: "found", view: data.site } : { domain: data.domain, kind: "missing" });
+ focusTitle.current = focus;
+ } catch (caught) {
+ if (controller.signal.aborted) return;
+ if (caught instanceof FieldError) {
+ setError(caught.message);
+ requestAnimationFrame(() => inputRef.current?.focus());
+ } else {
+ setOutcome({ domain: input.domain, kind: "error", message: caught instanceof Error ? caught.message : "The check didn't finish." });
+ focusTitle.current = focus;
+ }
+ } finally {
+ if (request.current === controller) {
+ request.current = null;
+ setLoading(false);
+ }
+ }
+ }
+
+ // A shared link checks its site right away. A clean URL waits for the button.
+ useEffect(() => {
+ if (initialSite) void check(initialSite, false);
+ }, [initialSite]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Stop a check still running when the tool goes away. A remount starts its own check.
+ useEffect(() => () => request.current?.abort(), []);
+
+ useEffect(() => {
+ if (!focusTitle.current || loading) return;
+ focusTitle.current = false;
+ titleRef.current?.focus({ preventScroll: true });
+ }, [outcome, loading]);
+
+ function submit(event: FormEvent) {
+ event.preventDefault();
+ void check(site);
+ }
+
+ /** Checks another site from inside the result, then brings the result back into view. */
+ function checkFromResult(domain: string) {
+ setSite(domain);
+ resultRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
+ void check(domain);
+ }
+
+ function reset() {
+ request.current?.abort();
+ setSite(EXAMPLE_SITE);
+ setError(null);
+ setOutcome(null);
+ setEdited(false);
+ setLoading(false);
+ }
+
+ const stale = loading || (edited && outcome !== null);
+
+ return (
+
+
+
+
+ {outcome?.kind === "found" ? (
+
+ ) : outcome?.kind === "missing" ? (
+
+ ) : outcome?.kind === "error" ? (
+ void check(outcome.domain)} titleRef={titleRef} />
+ ) : (
+
+ )}
+ {outcome ? : null}
+
+
+ );
+}
+
+/* ---------------- header ---------------- */
+
+type TitleRef = RefObject;
+
+/** The site's name as the result's title, with when we checked it and a link to the site (mockup page header). */
+function SiteHeader({ checkedAt, children, domain, favicon, share, title, titleRef }: {
+ checkedAt?: string | null;
+ children?: ReactNode;
+ domain: string | null;
+ favicon?: string | null;
+ share?: boolean;
+ title?: ReactNode;
+ titleRef?: TitleRef;
+}) {
+ return (
+
+
+
+ {/* data-gate: the API's recorded events (verifier joins and departures, later the Wayback run) fill the history. */}
+ {hasChanges(view) ? : null}
+ {/* data-gate: the ads-txt-summary run fills the counts, top sellers, and file preview. */}
+ {view.ads_txt_summary || view.ads_txt_preview ? : null}
+ {view.similar_sites.length ? : null}
+
: null}
+ We show the ad networks we've confirmed on the site, mostly from its ads.txt file.
+
+
+ );
+}
diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker-view.ts b/app/(site)/(tools)/ad-network-checker/ad-network-checker-view.ts
new file mode 100644
index 00000000..56c3af7b
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker-view.ts
@@ -0,0 +1,191 @@
+import type {
+ AdNetworkCheckerSiteView,
+ AdNetworkRef,
+ AdNetworkSiteRelationship,
+} from "@/lib/ad-networks-types";
+
+export const EXAMPLE_SITE = "collegefashion.net";
+export const NOT_LISTED = "Not listed";
+
+export type SiteInput = { domain: string; ok: true } | { message: string; ok: false };
+
+const LABEL = /^(?!-)[a-z0-9-]{1,63}(? 253
+ || labels.length < 2
+ || !labels.every((label) => LABEL.test(label))
+ || !ENDING.test(labels[labels.length - 1])
+ ) {
+ return { message: "Enter a domain such as example.com.", ok: false };
+ }
+ return { domain: host, ok: true };
+}
+
+const monthYear = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC", year: "numeric" });
+const fullDate = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "short", timeZone: "UTC", year: "numeric" });
+
+/** `Mar 2021`. */
+export function formatMonth(iso: string | null) {
+ return iso ? monthYear.format(new Date(iso)) : NOT_LISTED;
+}
+
+/** `Oct 12, 2026`. */
+export function formatDay(iso: string | null) {
+ return iso ? fullDate.format(new Date(iso)) : NOT_LISTED;
+}
+
+/** `2026-10`, the month key the history timeline takes. */
+export const monthKey = (iso: string) => iso.slice(0, 7);
+
+/** `Mediavine`, `Mediavine and Amazon APS`, `A, B, and C`. Pass `or` for `A, B, or C`. */
+export function names(list: readonly AdNetworkRef[], join: "and" | "or" = "and") {
+ if (list.length <= 2) return list.map((item) => item.name).join(` ${join} `);
+ return `${list.slice(0, -1).map((item) => item.name).join(", ")}, ${join} ${list[list.length - 1].name}`;
+}
+
+const statusOrder = { confirmed: 0, stale: 1, ended: 2 } as const;
+
+/** The manager first, then current networks, then ones not seen lately, then ended ones. */
+export function sortedRelationships(view: AdNetworkCheckerSiteView) {
+ const manager = view.site.managed_by?.slug;
+ return [...view.relationships].sort((a, b) =>
+ Number(b.slug === manager) - Number(a.slug === manager)
+ || statusOrder[a.status] - statusOrder[b.status]
+ || a.name.localeCompare(b.name));
+}
+
+/** True when this network is the site's main ad manager. */
+export const isManager = (view: AdNetworkCheckerSiteView, relationship: AdNetworkRef) =>
+ view.site.managed_by?.slug === relationship.slug;
+
+/**
+ * How we confirmed a network, from its stored verification method. Unknown methods stay unknown,
+ * and page-only evidence never reads as an ads.txt statement.
+ */
+export type Evidence = "manager" | "seller" | "page" | "unknown";
+
+export function evidenceOf(relationship: AdNetworkSiteRelationship): Evidence {
+ switch (relationship.verification_method) {
+ case "ads_txt_managerdomain": return "manager";
+ case "ads_txt_and_adsense_account":
+ case "ads_txt_and_page_signature": return "seller";
+ case "page_signature": return "page";
+ default: return "unknown";
+ }
+}
+
+/** What the site's ads.txt says about one network (mockup column), worded to match the evidence. */
+export function saysLabel(view: AdNetworkCheckerSiteView, relationship: AdNetworkSiteRelationship) {
+ const evidence = evidenceOf(relationship);
+ if (evidence === "page" && relationship.status !== "ended") return "Not listed. Its page ad code shows it";
+ if (evidence === "page" || evidence === "unknown") return NOT_LISTED;
+ if (relationship.status === "ended") return "No longer lists it";
+ if (evidence === "seller") return "Allows it to sell ad space";
+ return isManager(view, relationship) ? "Names it as the ad manager" : "Names it as an ad manager";
+}
+
+export function statusLabel(relationship: AdNetworkSiteRelationship) {
+ if (relationship.status === "confirmed") return "Confirmed live";
+ if (relationship.status === "stale") return "Not seen lately";
+ return relationship.ended_at ? `Ended ${formatMonth(relationship.ended_at)}` : "Ended";
+}
+
+/** Who to count: `it`, `both`, or `all 6`. */
+const countWord = (count: number) => (count === 1 ? "it" : count === 2 ? "both" : `all ${count}`);
+
+/** Each evidence group as a first sentence about the site, or a later one that adds to it. */
+const sentences: Record string> = {
+ manager: (list, domain, first) => `${first ? `${domain}'s ads.txt file names` : " Its ads.txt file also names"} ${names(list)} as ${list.length === 1 && first ? "its ad manager" : list.length === 1 ? "an ad manager" : "ad managers"}.`,
+ seller: (list, domain, first) => (first ? `${domain}'s ads.txt file lets ${names(list)} sell its ad space.` : ` It also lets ${names(list)} sell its ad space.`),
+ page: (list, domain, first) => (first ? `The ad code on ${domain}'s pages shows ${names(list)}.` : ` The ad code on its pages shows ${names(list)}.`),
+ unknown: (list, domain, first) => (first ? `We confirmed ${names(list)} on ${domain}.` : ` We also confirmed ${names(list)}.`),
+};
+
+/**
+ * The lead under the site's name, from the mockup: who runs its ads, who else sells its space,
+ * and when we confirmed them. Each network is worded by how we confirmed it, and each part
+ * appears only when the data behind it exists.
+ */
+export function leadFor(view: AdNetworkCheckerSiteView) {
+ const { domain, managed_by: manager, manager_domain: managerDomain } = view.site;
+ const rows = sortedRelationships(view);
+ const current = rows.filter((row) => row.status === "confirmed");
+ const stale = rows.filter((row) => row.status === "stale");
+ // "Runs" needs the MANAGERDOMAIN line: the site's own, or the relationship's stored method.
+ const lead = manager ? current.find((row) => row.slug === manager.slug && (managerDomain || evidenceOf(row) === "manager")) : undefined;
+ const parts: { manager?: AdNetworkRef; text: string }[] = [];
+ if (lead) parts.push({ manager: lead, text: `, and its ads.txt file names ${lead.name} as its ad manager.` });
+
+ for (const evidence of ["manager", "seller", "page", "unknown"] as const) {
+ const list = current.filter((row) => row !== lead && evidenceOf(row) === evidence);
+ if (list.length) parts.push({ text: sentences[evidence](list, domain, parts.length === 0) });
+ }
+ if (!parts.length) parts.push({ text: `We haven't confirmed a network we track on ${domain} in our latest checks.` });
+
+ const dates = current.map((row) => row.last_confirmed_at).filter((value): value is string => Boolean(value)).sort();
+ if (dates.length) {
+ const first = formatDay(dates[0]);
+ const last = formatDay(dates[dates.length - 1]);
+ const who = countWord(current.length);
+ parts.push({ text: first === last ? ` We confirmed ${who} live on ${last}.` : ` We last confirmed ${who} live between ${first} and ${last}.` });
+ }
+ if (stale.length) parts.push({ text: ` ${names(stale)} ${stale.length === 1 ? "hasn't" : "haven't"} shown up in our latest checks.` });
+ return parts;
+}
+
+export type HistoryEvent = { date: string; text: string };
+
+/**
+ * The site's network changes, newest first, from the events the API records (mockup events list).
+ * A move from one network to another comes only from the API's `switched` event. The first
+ * check reads as our starting point, since the networks on it may have been there long before.
+ */
+export function eventsFor(view: AdNetworkCheckerSiteView): HistoryEvent[] {
+ const events = [...(view.events ?? [])].sort((a, b) =>
+ Number(a.kind === "first_check") - Number(b.kind === "first_check") || b.date.localeCompare(a.date));
+ return events.map((event) => {
+ const date = formatMonth(event.date);
+ if (event.kind === "switched" && event.from_network && event.network) return { date, text: `Moved from ${event.from_network.name} to ${event.network.name}` };
+ if (event.kind === "first_check") {
+ const seen = event.networks?.length ? event.networks : event.network ? [event.network] : [];
+ return { date, text: seen.length ? `Our first check, with ${names(seen)}` : "Our first check" };
+ }
+ const name = event.network?.name ?? event.from_network?.name;
+ if (!name) return null;
+ return { date, text: event.kind === "dropped" ? `Dropped ${name}` : `Added ${name}` };
+ }).filter((event): event is HistoryEvent => event !== null);
+}
+
+/** Real changes, not only what our first check found. */
+export const hasChanges = (view: AdNetworkCheckerSiteView) =>
+ (view.events ?? []).some((event) => event.kind !== "first_check") || view.relationships.some((item) => item.status === "ended");
+
+/** The day of our first stored check on this site, when the history starts. */
+export function firstCheck(view: AdNetworkCheckerSiteView) {
+ return view.events?.find((event) => event.kind === "first_check")?.date ?? null;
+}
+
+/** The networks the similar sites were matched on, the same rule as the site view loader. */
+export function similarNetworks(view: AdNetworkCheckerSiteView): AdNetworkRef[] {
+ const current = sortedRelationships(view).filter((item) => item.status === "confirmed");
+ const manager = view.site.managed_by;
+ return manager && current.some((item) => item.slug === manager.slug) ? [manager] : current;
+}
+
+export function adsTxtUrl(domain: string) {
+ return `https://${domain}/ads.txt`;
+}
diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker.module.css b/app/(site)/(tools)/ad-network-checker/ad-network-checker.module.css
new file mode 100644
index 00000000..b6a9503a
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker.module.css
@@ -0,0 +1,170 @@
+.form {
+ display: grid;
+ gap: var(--space-5);
+ min-width: 0;
+}
+
+/* The result follows the mockup's site view: header, facts, then sections split by hairlines. */
+.result {
+ gap: 30px;
+}
+
+/* Site header (mockup .page-header): title with the site in a highlight, meta line, lead. */
+.header {
+ display: grid;
+ gap: var(--space-5);
+ min-width: 0;
+}
+
+.titleRow {
+ align-items: flex-start;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-5) var(--space-7);
+ justify-content: space-between;
+ min-width: 0;
+}
+
+.title {
+ color: var(--color-text);
+ font: var(--weight-bold) var(--text-h1) / var(--line-h1) var(--font-display);
+ isolation: isolate;
+ letter-spacing: var(--tracking-display);
+ margin: 0;
+ max-width: 820px;
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.title:focus {
+ outline: none;
+}
+
+.title:focus-visible {
+ box-shadow: var(--focus-ring);
+}
+
+.title mark {
+ -webkit-box-decoration-break: clone;
+ background: var(--color-accent);
+ border-radius: var(--radius);
+ box-decoration-break: clone;
+ color: var(--color-accent-contrast);
+ padding-inline: 0.16em;
+ position: relative;
+ z-index: -1;
+}
+
+.meta {
+ align-items: center;
+ color: var(--color-text-muted);
+ display: flex;
+ flex-wrap: wrap;
+ font: var(--text-small) / 1.4 var(--font-mono);
+ gap: var(--space-2) var(--space-7);
+}
+
+.meta span,
+.meta a {
+ align-items: center;
+ display: inline-flex;
+ gap: var(--space-2);
+ white-space: nowrap;
+}
+
+.meta a {
+ color: inherit;
+ text-decoration: underline;
+ text-decoration-color: var(--color-border);
+ text-underline-offset: 3px;
+}
+
+.meta a:hover {
+ color: var(--color-text);
+ text-decoration-color: currentColor;
+}
+
+.lead {
+ margin: 0;
+ max-width: var(--measure-prose);
+ overflow-wrap: anywhere;
+}
+
+.lead a {
+ color: var(--color-link);
+ text-underline-offset: 3px;
+}
+
+.lead a:hover {
+ text-decoration: underline;
+}
+
+/* Sections (mockup .nsec.site-sec): every one opens with a hairline, including the first. */
+.sections {
+ display: grid;
+ min-width: 0;
+}
+
+.sections > section:nth-child(n) {
+ border-top: 1px solid var(--color-border);
+ margin-top: 6px;
+ padding-top: 38px;
+}
+
+.sections > section:nth-child(n + 2) {
+ margin-top: 44px;
+}
+
+/* ads.txt: numbers and bars beside the file, split by a hairline (mockup .pair). */
+.block {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-5);
+ min-width: 0;
+}
+
+.pair {
+ display: grid;
+ gap: 34px;
+}
+
+@media (min-width: 900px) {
+ .pair {
+ gap: 0;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ }
+
+ .pair > .block:first-child {
+ padding-right: 32px;
+ }
+
+ .pair > .block + .block {
+ border-left: 1px solid var(--color-border);
+ padding-left: 32px;
+ }
+}
+
+/* Check another site (mockup .check-form). */
+.another {
+ display: grid;
+ gap: var(--space-3);
+ grid-template-columns: minmax(0, 520px) max-content;
+ justify-content: start;
+}
+
+.anotherButton {
+ height: 52px;
+ padding-inline: 22px;
+}
+
+.error {
+ color: var(--color-danger-text);
+ font-size: var(--text-small);
+ margin: 0;
+}
+
+@media (max-width: 640px) {
+ .another {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/app/(site)/(tools)/ad-network-checker/content/article.mdx b/app/(site)/(tools)/ad-network-checker/content/article.mdx
new file mode 100644
index 00000000..93c3372f
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/content/article.mdx
@@ -0,0 +1,41 @@
+## How do I find out which ad network a website uses?
+
+Open the site's ads.txt file by adding `/ads.txt` to its address, such as `example.com/ads.txt`. Look for a line that starts with `MANAGERDOMAIN`. If it's there, it names the company that manages the site's ads.
+
+Without that line, the file is a list of companies allowed to sell the site's ad space. Spotting the network in that list takes practice, because most lines belong to the network's partners. This checker does that reading for you and matches the lines to the networks we track.
+
+You can also look at the page's code for ad scripts, but scripts change often and some load only for certain visitors. The ads.txt file is public, stable, and written by the site owner, so we rely on it.
+
+## What does MANAGERDOMAIN mean in ads.txt?
+
+`MANAGERDOMAIN` names the primary or exclusive company that sells a site's ad space, as defined in the [IAB ads.txt 1.1 spec](https://iabtechlab.com/wp-content/uploads/2022/04/Ads.txt-1.1.pdf). Most of the network links we confirm come from this line.
+
+It doesn't prove the deal is exclusive. A site can name a manager and still let other networks sell its ad space, which is why the result can show one main ad network and still list others.
+
+`OWNERDOMAIN` is a different line. It names the business that owns the site, not an ad network.
+
+## Why does the ads.txt file list so many companies?
+
+Each line authorizes one ad account to sell the site's space. `DIRECT` means the site's own account with that company. `RESELLER` means a company that sells the space on someone else's behalf, usually the site's ad network.
+
+A network such as Mediavine works with dozens of ad exchanges, and each one needs its own line. That's why one network can add dozens of lines. The [IAB's ads.txt page](https://iabtechlab.com/ads-txt/) explains the format in full.
+
+## Why isn't a site in the results?
+
+We only show sites our checker has read. We find sites from public lists and each network's own seller list, then read their ads.txt on a schedule. A new or small site may not be in our list yet.
+
+A site also stays out when we can't confirm a network we track. That includes sites with no ads.txt file, sites that block our checker, and sites that use a network outside our list. The [ad networks directory](/ad-networks) lists every network we track.
+
+## How current is the result?
+
+The `Checked` date under the site name is our latest read of its ads.txt. We recheck sites on a schedule.
+
+The `Since` month is when we first confirmed a network. If that was our first check of the site, the network may have been there for years before. The changes section says when our checks of the site started.
+
+One missed check never removes a network. A timeout or a blocked request counts as unknown. A network moves to `Not seen lately`, then `Ended`, only after repeated checks find it missing.
+
+## Does stack127 show traffic or earnings?
+
+Not yet. Traffic and niche join the site facts as our data runs finish. We don't estimate earnings, because there's no public source we can check them against.
+
+If you're choosing a network for your own site, the [ad networks directory](/ad-networks) shows each network's sites and how to join.
diff --git a/app/(site)/(tools)/ad-network-checker/content/intro.mdx b/app/(site)/(tools)/ad-network-checker/content/intro.mdx
new file mode 100644
index 00000000..9f3590ef
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/content/intro.mdx
@@ -0,0 +1 @@
+Enter a website to see which ad networks run its ads, whether one of them manages the site, and since when we've seen it. The answer comes from the site's own ads.txt file, checked against the networks we track.
diff --git a/app/(site)/(tools)/ad-network-checker/content/usage.mdx b/app/(site)/(tools)/ad-network-checker/content/usage.mdx
new file mode 100644
index 00000000..a2a2cf1f
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/content/usage.mdx
@@ -0,0 +1,35 @@
+## How do I read the result?
+
+Enter a domain or any page address on the site, then press `Check site`. The sentence under the site's name gives the answer: who runs its ads, who else can sell its ad space, and when we last confirmed them live.
+
+Those words follow how we confirmed each network. A network is the site's `main ad network`, and the sentence says the site runs it, only when a `MANAGERDOMAIN` line in its ads.txt names it. For other networks in the file, the sentence says the file lets them sell its ad space, because a listing alone doesn't show which one is in charge. When we confirmed a network from the ad code on the site's pages instead, the sentence says that and doesn't claim the ads.txt file names it.
+
+The row of facts under it describes the site: main ad network, traffic, niche, platform, the year its domain was registered, and its publisher. Each fact shows only when we know it, so a site we know little about has a short row. The same goes for the sections below it.
+
+## What do the statuses in the networks table mean?
+
+The table lists every network we've linked to the site, including ones that stopped. `What its ads.txt says` shows whether the file names the network as an ad manager or allows it to sell ad space. It says `Not listed` when we confirmed the network from the site's page ad code, or when we don't know how we confirmed it. `Since` is the month we first confirmed it.
+
+| Status | What it means |
+| --- | --- |
+| Confirmed live | Our last check found the network on the site. |
+| Not seen lately | Two checks at least 48 hours apart didn't find it. It may come back. |
+| Ended, with a month | Three checks over seven days or more didn't find it. The month is when it ended. |
+
+## What do the other sections show?
+
+| Section | What it shows |
+| --- | --- |
+| How have its ad networks changed? | One bar per network, from the month it first appeared to the month it last did, and a list of each change, newest first. It says a site moved from one network to another only when one ad manager replaced another within seven days. The section shows once a site has a real change, such as a network that left. |
+| What is in its ads.txt file? | How many lines, companies, direct lines, and reseller lines the file has, the companies with the most lines, and the start of the file. |
+| Similar sites | Other sites on the same main ad network, or on the same networks when there's no `MANAGERDOMAIN` line. Click one to check it here. |
+
+A food blog with one ad network can still list dozens of companies in its ads.txt, because the network brings its own partners. The companies with the most lines are usually those partners, not separate networks.
+
+
+
+Its ads.txt has the line `MANAGERDOMAIN=mediavine.com`, so the sentence reads `collegefashion.net runs Mediavine, and its ads.txt file names Mediavine as its ad manager.` Mediavine is its main ad network and shows as `Confirmed live` in the networks table.
+
+
+
+Use `Copy link` to share the result. The link opens this page and checks the same site.
diff --git a/app/(site)/(tools)/ad-network-checker/layout.tsx b/app/(site)/(tools)/ad-network-checker/layout.tsx
new file mode 100644
index 00000000..ff25ef9d
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/layout.tsx
@@ -0,0 +1,6 @@
+import { registryLayout } from "@/lib/registry-layout";
+
+const registry = registryLayout("/ad-network-checker");
+export const generateMetadata = registry.generateMetadata;
+export const revalidate = false;
+export default registry.Layout;
diff --git a/app/(site)/(tools)/ad-network-checker/page.tsx b/app/(site)/(tools)/ad-network-checker/page.tsx
new file mode 100644
index 00000000..7ca4908a
--- /dev/null
+++ b/app/(site)/(tools)/ad-network-checker/page.tsx
@@ -0,0 +1,51 @@
+import { readdirSync } from "node:fs";
+import path from "node:path";
+import { ScanSearch } from "lucide-react";
+import { PageCanvas } from "@/app/ui/site/page-canvas";
+import type { SectionNav } from "@/app/ui/site/section-bar";
+import { ToolPageArticle, ToolPageHeader } from "@/app/ui/site/tool-page-content";
+import { ToolPageShell } from "@/app/ui/tools/tool-page-shell";
+import { getRequiredToolPage } from "@/lib/tool-pages";
+import { AdNetworkCheckerFromUrl } from "./ad-network-checker-from-url";
+import Article from "./content/article.mdx";
+import Intro from "./content/intro.mdx";
+import Usage from "./content/usage.mdx";
+
+const SLUG = "ad-network-checker";
+
+/** Every network logo by slug, read once at build time so the browser-side result can show them. */
+function networkLogos() {
+ const folder = path.join(process.cwd(), "public", "ad-networks", "logos");
+ return Object.fromEntries(readdirSync(folder)
+ .filter((file) => file.endsWith(".png"))
+ .map((file) => [file.slice(0, -4), `/ad-networks/logos/${file}`]));
+}
+
+const sectionNav: SectionNav = {
+ label: "Ad networks",
+ href: "/ad-networks",
+ current: `/${SLUG}`,
+ items: [
+ { label: "Directory", href: "/ad-networks" },
+ { label: "Niches", href: "/ad-networks/niches" },
+ { label: "Stats", href: "/ad-networks/stats" },
+ { label: "Checker", href: `/${SLUG}` },
+ ],
+};
+
+export default async function AdNetworkCheckerPage() {
+ const toolPage = await getRequiredToolPage(SLUG);
+
+ return (
+
+
+ } intro={} toolPage={toolPage} />
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/api/ad-networks/input.ts b/app/api/ad-networks/input.ts
new file mode 100644
index 00000000..a51b09a5
--- /dev/null
+++ b/app/api/ad-networks/input.ts
@@ -0,0 +1,53 @@
+import { domainToASCII } from "node:url";
+import { isIP } from "node:net";
+import { getDomain } from "tldts";
+import { AD_NETWORK_SITES_PAGE_SIZE } from "../../../lib/ad-networks-report.ts";
+import { getAdNetworkNiche } from "../../../lib/ad-network-niches.ts";
+import type { AdNetworkNicheSlug } from "../../../lib/ad-network-niches.ts";
+import type { AdNetworkSitesQuery, AdNetworkSitesSort } from "../../../lib/ad-networks-types.ts";
+
+export class AdNetworkInputError extends Error {}
+const sorts = new Set(["domain", "traffic", "oldest", "newest", "checked"]);
+const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+export function parseAdNetworkSitesParams(params: URLSearchParams): AdNetworkSitesQuery {
+ const allowed = new Set(["network", "niche", "q", "sort", "page", "pageSize"]);
+ for (const key of params.keys()) {
+ if (!allowed.has(key) || params.getAll(key).length > 1) throw new AdNetworkInputError(`Invalid parameter: ${key}`);
+ }
+ const network = params.get("network") || null;
+ const niche = params.get("niche") || null;
+ const q = (params.get("q") ?? "").trim().toLowerCase();
+ const sort = params.get("sort") ?? "domain";
+ if (network && (network.length > 100 || !slug.test(network))) throw new AdNetworkInputError("Invalid network.");
+ if (niche && !getAdNetworkNiche(niche)) throw new AdNetworkInputError("Invalid niche.");
+ if (q.length > 100 || /[\x00-\x1f\x7f]/.test(q)) throw new AdNetworkInputError("Search must be at most 100 characters.");
+ if (!sorts.has(sort)) throw new AdNetworkInputError("Invalid sort.");
+ function integer(key: string, fallback: number, max: number) {
+ const raw = params.get(key);
+ if (raw === null) return fallback;
+ if (!/^[1-9]\d*$/.test(raw) || Number(raw) > max) throw new AdNetworkInputError(`Invalid ${key}.`);
+ return Number(raw);
+ }
+ return { network, niche: niche as AdNetworkNicheSlug | null, q, sort: sort as AdNetworkSitesSort,
+ page: integer("page", 1, 10000), pageSize: integer("pageSize", AD_NETWORK_SITES_PAGE_SIZE, 100) };
+}
+export function parseAdNetworkSiteParams(params: URLSearchParams): string {
+ if ([...params.keys()].some(key => key !== "domain") || params.getAll("domain").length !== 1) {
+ throw new AdNetworkInputError("Supply one domain.");
+ }
+ const input = params.get("domain")?.trim() ?? "";
+ if (!input || input.length > 2048 || /[\s@*]/.test(input)) throw new AdNetworkInputError("Enter a public website domain.");
+ let url: URL;
+ try { url = new URL(/^https?:\/\//i.test(input) ? input : `https://${input}`); }
+ catch { throw new AdNetworkInputError("Enter a valid domain."); }
+ const domain = domainToASCII(url.hostname.toLowerCase().replace(/\.$/, "")).replace(/^www\./, "");
+ const labels = domain.split(".");
+ if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.port || isIP(domain)
+ || domain.length > 253 || labels.length < 2
+ || labels.some(label => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
+ || !getDomain(domain, { allowPrivateDomains: true })
+ || /\.(?:local|localhost|internal|invalid|test|example)$/.test(domain)) {
+ throw new AdNetworkInputError("Enter a public website domain.");
+ }
+ return domain;
+}
diff --git a/app/api/ad-networks/site/route.ts b/app/api/ad-networks/site/route.ts
new file mode 100644
index 00000000..9b8da8ef
--- /dev/null
+++ b/app/api/ad-networks/site/route.ts
@@ -0,0 +1,27 @@
+import { NextResponse } from "next/server";
+import { getAdNetworkCheckerSite } from "@/lib/ad-networks";
+import { getPublishedPage } from "@/lib/page-registry";
+import { AdNetworkInputError, parseAdNetworkSiteParams } from "../input";
+import type { AdNetworkSiteResponse } from "@/lib/ad-networks-types";
+
+export const dynamic = "force-dynamic";
+/** Lookup only: unknown domains never trigger a network request or a database write. */
+export async function GET(request: Request) {
+ const noStore = { "Cache-Control": "no-store" };
+ try {
+ const domain = parseAdNetworkSiteParams(new URL(request.url).searchParams);
+ if (!await getPublishedPage("ad-network-checker")) {
+ return NextResponse.json({ error: "Tool not found." }, { status: 404, headers: noStore });
+ }
+ const site = await getAdNetworkCheckerSite(domain);
+ const result: AdNetworkSiteResponse = { domain, found: site !== null, site };
+ return NextResponse.json(result, { headers: site ? {
+ "Cache-Control": "public, max-age=0, must-revalidate, s-maxage=604800",
+ "Cache-Tag": "stack127-public,ad-networks-catalog", "X-Robots-Tag": "noindex, follow",
+ } : noStore });
+ } catch (error) {
+ if (error instanceof AdNetworkInputError) return NextResponse.json({ error: error.message }, { status: 400, headers: noStore });
+ console.error("Ad-network checker failed", error);
+ return NextResponse.json({ error: "Could not load this site." }, { status: 503, headers: noStore });
+ }
+}
diff --git a/app/api/ad-networks/sites/route.ts b/app/api/ad-networks/sites/route.ts
new file mode 100644
index 00000000..86b8b1eb
--- /dev/null
+++ b/app/api/ad-networks/sites/route.ts
@@ -0,0 +1,25 @@
+import { NextResponse } from "next/server";
+import { getAdNetworkSites } from "@/lib/ad-networks";
+import { getPublishedPage } from "@/lib/page-registry";
+import { AdNetworkInputError, parseAdNetworkSitesParams } from "../input";
+
+export const dynamic = "force-dynamic";
+export async function GET(request: Request) {
+ const noStore = { "Cache-Control": "no-store" };
+ try {
+ const query = parseAdNetworkSitesParams(new URL(request.url).searchParams);
+ if (!await getPublishedPage("ad-networks") || (query.network && !await getPublishedPage(`ad-networks/${query.network}`))
+ || (query.niche && !await getPublishedPage(`ad-networks/niches/${query.niche}`))) {
+ return NextResponse.json({ error: "Directory not found." }, { status: 404, headers: noStore });
+ }
+ const result = await getAdNetworkSites(query);
+ return NextResponse.json(result, { headers: {
+ "Cache-Control": "public, max-age=0, must-revalidate, s-maxage=604800",
+ "Cache-Tag": "stack127-public,ad-networks-catalog", "X-Robots-Tag": "noindex, follow",
+ } });
+ } catch (error) {
+ if (error instanceof AdNetworkInputError) return NextResponse.json({ error: error.message }, { status: 400, headers: noStore });
+ console.error("Ad-network sites failed", error);
+ return NextResponse.json({ error: "Could not load sites." }, { status: 503, headers: noStore });
+ }
+}
diff --git a/app/api/cache/refresh/route.ts b/app/api/cache/refresh/route.ts
index 7a0517b4..2ee076a8 100644
--- a/app/api/cache/refresh/route.ts
+++ b/app/api/cache/refresh/route.ts
@@ -13,6 +13,7 @@ import { loadSelfHostAppsSearch } from "@/lib/self-host-apps-search-data";
import { parseSelfHostAppsSearchParams } from "@/lib/self-host-apps-search";
import { loadSelfHostedAppResourcePlannerEntry } from "@/lib/self-hosted-app-resource-planner/data";
import { getPool } from "@/lib/db";
+import { adNetworkPageKind, getAdNetworkApiVerification } from "@/lib/ad-networks-cache-verification";
import { sitePageAvailable } from "@/lib/page-source";
import { getFeeGroup } from "@/lib/platform-fees/groups";
import { LAUNCH_DIRECTORY_REVISION_SQL, launchDirectoryRevisionMatches } from "@/lib/launch-directories-data";
@@ -29,14 +30,17 @@ export async function POST(request: Request) {
if (!Array.isArray(paths) || paths.length > 20 || paths.some(path => typeof path !== "string" || !/^\/[a-z0-9/_-]*(?:\.xml)?$/.test(path))) {
return NextResponse.json({ error: "Invalid paths" }, { status: 400, headers: noStore });
}
- const verified: Array<{ path: string; status: number; revision: string | null; digest: string | null; sourceRevision: string | null }> = [];
+ const verified: Array<{ path: string; status: number; revision: string | null; digest: string | null; sourceRevision: string | null; requestPath?: string }> = [];
for (let i = 0; i < paths.length; i += 4) {
await Promise.all(paths.slice(i, i + 4).map(async (path: string) => {
+ const adNetworkVerification = path === "/api/ad-networks/sites" || path === "/api/ad-networks/site"
+ ? await getAdNetworkApiVerification(path) : null;
+ const requestPath=adNetworkVerification?.requestPath??path;
const hosts = [...new Set(["127.0.0.1", process.env.HOSTNAME].filter(Boolean))];
let response: Response | undefined;
for (const host of hosts) {
try {
- response = await fetch(`http://${host}:${process.env.PORT || "3333"}${path}`, {
+ response = await fetch(`http://${host}:${process.env.PORT || "3333"}${requestPath}`, {
cache: "no-store", redirect: "manual", signal: AbortSignal.timeout(15_000),
});
break;
@@ -47,23 +51,26 @@ export async function POST(request: Request) {
let contentRevision: string | null = null;
let sourceRevision: string | null = null;
if (path.startsWith("/api/")) {
- const slug = path.split("/").at(-1)!;
- const hardwareDirectory = path.startsWith("/api/hardware-directory/") ? slug : null;
- const parent = hardwareDirectory ?? (path === "/api/cli-apps/index" ? "cli-apps"
- : path.startsWith("/api/self-host-apps/") ? "self-host-apps" : "self-hosted-app-resource-planner");
- const visible = (await getPool().query("select is_published from public.stack127_pages where slug=$1", [parent])).rows[0]?.is_published;
let expected: unknown;
- if (!visible) expected = null;
- else if (hardwareDirectory === "zigbee-devices") {
- expected = (await addAmazonMatches(hardwareDirectory, getZigbeeDevices()))
- .map(device => zigbeeIndexRecord(device, zigbeeCategoryLabels));
- } else if (hardwareDirectory && isHardwareDirectorySlug(hardwareDirectory)) {
- expected = (await getHardwareDirectorySnapshot(hardwareDirectory)).records.map(hardwareIndexRecord);
- } else if (path === "/api/cli-apps/index") expected = await loadTerminalApps();
- else if (path === "/api/self-host-apps/search") {
- expected = await loadSelfHostAppsSearch(parseSelfHostAppsSearchParams(new URLSearchParams()));
- } else if (path.startsWith("/api/self-host-apps/")) expected = (await loadDirectoryAppsBySlugs([slug]))[0] ?? null;
- else if (path.startsWith("/api/resource-planner-app/")) expected = await loadSelfHostedAppResourcePlannerEntry(slug);
+ if (adNetworkVerification) expected=adNetworkVerification.expected;
+ else {
+ const slug = path.split("/").at(-1)!;
+ const hardwareDirectory = path.startsWith("/api/hardware-directory/") ? slug : null;
+ const parent = hardwareDirectory ?? (path === "/api/cli-apps/index" ? "cli-apps"
+ : path.startsWith("/api/self-host-apps/") ? "self-host-apps" : "self-hosted-app-resource-planner");
+ const visible = (await getPool().query("select is_published from public.stack127_pages where slug=$1", [parent])).rows[0]?.is_published;
+ if (!visible) expected = null;
+ else if (hardwareDirectory === "zigbee-devices") {
+ expected = (await addAmazonMatches(hardwareDirectory, getZigbeeDevices()))
+ .map(device => zigbeeIndexRecord(device, zigbeeCategoryLabels));
+ } else if (hardwareDirectory && isHardwareDirectorySlug(hardwareDirectory)) {
+ expected = (await getHardwareDirectorySnapshot(hardwareDirectory)).records.map(hardwareIndexRecord);
+ } else if (path === "/api/cli-apps/index") expected = await loadTerminalApps();
+ else if (path === "/api/self-host-apps/search") {
+ expected = await loadSelfHostAppsSearch(parseSelfHostAppsSearchParams(new URLSearchParams()));
+ } else if (path.startsWith("/api/self-host-apps/")) expected = (await loadDirectoryAppsBySlugs([slug]))[0] ?? null;
+ else if (path.startsWith("/api/resource-planner-app/")) expected = await loadSelfHostedAppResourcePlannerEntry(slug);
+ }
if (expected === undefined) throw new Error("Unsupported API refresh");
if (!expected) {
if (response.status !== 404) throw new Error(`${path}: removed data is still served`);
@@ -90,8 +97,11 @@ export async function POST(request: Request) {
);
let published = sitePageAvailable(path, rows[0]);
if (published && path.startsWith("/ad-networks/")) {
- const source = await getPool().query("select 1 from public.ad_networks where slug=$1", [path.slice("/ad-networks/".length)]);
- published = source.rows.length > 0;
+ const kind=adNetworkPageKind(path);
+ if (kind === "network") {
+ const source = await getPool().query("select 1 from public.ad_networks where slug=$1", [path.slice("/ad-networks/".length)]);
+ published = source.rows.length > 0;
+ } else if (kind === "missing") published=false;
} else if (published && path.startsWith("/platform-fees/")) {
const group = getFeeGroup(path.slice("/platform-fees/".length));
if (!group) published = false;
@@ -121,7 +131,7 @@ export async function POST(request: Request) {
}
}
}
- verified.push({ path, status: response.status, revision: contentRevision, digest: path.startsWith("/api/") && response.status === 200 ? await cacheBodyDigest(html) : null, sourceRevision });
+ verified.push({ path, ...(requestPath!==path ? {requestPath} : {}), status: response.status, revision: contentRevision, digest: path.startsWith("/api/") && response.status === 200 ? await cacheBodyDigest(html) : null, sourceRevision });
}));
}
return NextResponse.json({ verified }, { headers: noStore });
diff --git a/app/ui/ad-networks/confirmed-ring.tsx b/app/ui/ad-networks/confirmed-ring.tsx
new file mode 100644
index 00000000..466bd49f
--- /dev/null
+++ b/app/ui/ad-networks/confirmed-ring.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { formatShare } from "./viz/format";
+import styles from "./network-card.module.css";
+
+const radius = 15;
+const circumference = 2 * Math.PI * radius;
+
+/**
+ * A small ring filled by the share of a network's sites we confirmed live, with a bubble saying it
+ * in words. Hover or keyboard focus shows the bubble; a tap toggles it on touch screens, and a tap
+ * elsewhere or Escape closes it.
+ */
+export function ConfirmedRing({ share }: { share: number }) {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+ const percent = formatShare(share);
+
+ useEffect(() => {
+ if (!open) return;
+ const away = (event: PointerEvent) => { if (!ref.current?.contains(event.target as Node)) setOpen(false); };
+ const escape = (event: KeyboardEvent) => { if (event.key === "Escape") setOpen(false); };
+ document.addEventListener("pointerdown", away);
+ document.addEventListener("keydown", escape);
+ return () => {
+ document.removeEventListener("pointerdown", away);
+ document.removeEventListener("keydown", escape);
+ };
+ }, [open]);
+
+ return (
+
+ );
+}
diff --git a/app/ui/ad-networks/details.module.css b/app/ui/ad-networks/details.module.css
new file mode 100644
index 00000000..1494e48b
--- /dev/null
+++ b/app/ui/ad-networks/details.module.css
@@ -0,0 +1,172 @@
+.details {
+ display: grid;
+ gap: var(--space-8);
+ min-width: 0;
+}
+
+.body {
+ display: grid;
+ gap: var(--space-13);
+}
+
+.main {
+ align-content: start;
+ display: grid;
+ gap: var(--space-8);
+ max-width: 980px;
+ min-width: 0;
+}
+
+.main > h2 {
+ font: var(--weight-bold) var(--text-h2) / var(--line-heading) var(--font-display);
+ letter-spacing: var(--tracking-display);
+ margin: 0;
+}
+
+.prose {
+ align-content: start;
+ display: grid;
+ gap: var(--space-8);
+ max-width: var(--measure-prose);
+}
+
+.prose section {
+ display: grid;
+ gap: var(--space-3);
+}
+
+.prose h3 {
+ font: var(--weight-semibold) 1.12rem / 1.3 var(--font-display);
+ letter-spacing: -0.01em;
+ margin: 0;
+}
+
+.prose p {
+ color: var(--color-text);
+ line-height: var(--line-body);
+ margin: 0;
+}
+
+.prose a {
+ color: var(--color-link);
+ text-underline-offset: 3px;
+}
+
+@media (min-width: 1200px) {
+ .body {
+ gap: 56px;
+ grid-template-columns: minmax(300px, 1fr) minmax(0, 1.45fr);
+ }
+
+ .prose {
+ border-right: 1px solid var(--color-border);
+ order: -1;
+ padding-right: var(--space-13);
+ }
+}
+
+/* Key numbers */
+.keyNumbers {
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ display: grid;
+ grid-template-columns: repeat(var(--count), minmax(0, 1fr));
+ margin: 0;
+}
+
+.keyNumbers > div {
+ border-left: 1px solid var(--color-border);
+ display: flex;
+ flex-direction: column-reverse;
+ gap: var(--space-2);
+ justify-content: flex-end;
+ min-width: 0;
+ padding: var(--space-7) var(--space-8);
+}
+
+.keyNumbers > div:first-child {
+ border-left: 0;
+}
+
+.keyNumbers dt {
+ color: var(--color-text-muted);
+ font: var(--text-small) var(--font-mono);
+}
+
+.keyNumbers dt small {
+ display: block;
+ font-size: 0.72rem;
+ line-height: 1.35;
+ margin-top: 2px;
+ opacity: 0.85;
+}
+
+.keyNumbers dd {
+ font: var(--weight-semibold) 1.6rem / 1.1 var(--font-mono);
+ letter-spacing: -0.02em;
+ margin: 0;
+}
+
+/* Facts */
+.facts {
+ margin: 0;
+}
+
+.facts > div {
+ border-top: 1px solid var(--color-border);
+ display: grid;
+ gap: var(--space-2) var(--space-8);
+ grid-template-columns: minmax(150px, 30%) 1fr;
+ padding: var(--space-5) 0;
+}
+
+.facts > div:first-child {
+ border-top: 0;
+}
+
+.facts dt {
+ color: var(--color-text-muted);
+}
+
+.facts dd {
+ margin: 0;
+}
+
+.facts dd small {
+ color: var(--color-text-muted);
+ display: block;
+ font-size: var(--text-small);
+ margin-top: 2px;
+}
+
+.notPublic {
+ color: var(--color-text-muted);
+ font: var(--text-small) var(--font-mono);
+}
+
+.sources {
+ color: var(--color-text-muted);
+ font: var(--text-small) var(--font-mono);
+ margin: 0;
+}
+
+.sources a {
+ color: inherit;
+ text-decoration: underline;
+ text-decoration-color: var(--color-border-strong);
+ text-underline-offset: 3px;
+}
+
+@media (max-width: 700px) {
+ .keyNumbers > div {
+ padding: var(--space-6) var(--space-5);
+ }
+
+ .keyNumbers dd {
+ font-size: 1.25rem;
+ }
+
+ .facts > div {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/app/ui/ad-networks/details.tsx b/app/ui/ad-networks/details.tsx
new file mode 100644
index 00000000..8fa89826
--- /dev/null
+++ b/app/ui/ad-networks/details.tsx
@@ -0,0 +1,87 @@
+import type { CSSProperties, ReactNode } from "react";
+import styles from "./details.module.css";
+
+export type KeyNumber = {
+ label: string;
+ value: ReactNode;
+ /** A short caveat under the label, such as `in older public terms`. */
+ note?: ReactNode;
+};
+
+/** Up to three headline facts in one bordered row, value above label (mockup `dl.key-nums`). */
+export function KeyNumbers({ items }: { items: readonly KeyNumber[] }) {
+ return (
+
+ {items.map((item) => (
+
+
{item.label}{item.note ? {item.note} : null}
+
{item.value}
+
+ ))}
+
+ );
+}
+
+export type Fact = {
+ label: string;
+ value: ReactNode;
+ /** A small muted line under the value, such as `Measured in Google Analytics`. */
+ note?: ReactNode;
+};
+
+/** Label and value rows split by hairlines, with an optional small note (mockup `dl.facts`). */
+export function FactsList({ items }: { items: readonly Fact[] }) {
+ return (
+
+ {items.map((item) => (
+
+
{item.label}
+
{item.value}{item.note ? {item.note} : null}
+
+ ))}
+
+ );
+}
+
+/** For a fact the network doesn't publish. Research says so; it isn't missing data. */
+export function NotPublic() {
+ return Not public;
+}
+
+/**
+ * The network details area (mockup `.details-body`): the facts column with its h2, key numbers,
+ * tabs, and sources, and the guide prose beside it. On wide screens the prose sits on the left
+ * behind a hairline; below 1200px it follows the facts.
+ */
+export function DetailsLayout({ children, prose, title, titleId }: {
+ /** Key numbers, tabs, and the sources line. */
+ children: ReactNode;
+ /** The Q&A prose, as h3 sections. */
+ prose?: ReactNode;
+ title: ReactNode;
+ titleId: string;
+}) {
+ return (
+
+
+
+
{title}
+ {children}
+
+ {prose ?
{prose}
: null}
+
+
+ );
+}
+
+/** Where the facts come from and when we checked (mockup `.src`). */
+export function SourcesLine({ checked, sources }: { checked?: string; sources: readonly { label: string; href: string }[] }) {
+ return (
+
+ ) : null}
+
+ );
+}
+
+/** Network cards four across on wide screens, down to one on phones (mockup `.net-grid`). */
+export function NetworkGrid({ children }: { children: ReactNode }) {
+ return
{children}
;
+}
+
+/** One mono line under the section heading saying how the cards are sorted (mockup `.sort-note`). */
+export function SortNote({ children }: { children: ReactNode }) {
+ return
{children}
;
+}
diff --git a/app/ui/ad-networks/network-ladder.module.css b/app/ui/ad-networks/network-ladder.module.css
new file mode 100644
index 00000000..387e791d
--- /dev/null
+++ b/app/ui/ad-networks/network-ladder.module.css
@@ -0,0 +1,84 @@
+.table {
+ border-collapse: collapse;
+ font-size: 0.97rem;
+ width: 100%;
+}
+
+.table thead th {
+ border-bottom: 1px solid var(--color-border-strong);
+ color: var(--color-text-muted);
+ font: 500 0.8rem var(--font-mono);
+ padding: 0 var(--space-5) var(--space-4);
+ text-align: left;
+ white-space: nowrap;
+}
+
+.table tbody th,
+.table tbody td {
+ border-top: 1px solid var(--color-border);
+ font-weight: var(--weight-regular);
+ padding: var(--space-5);
+ text-align: left;
+ white-space: nowrap;
+}
+
+.table tbody tr:first-child > * {
+ border-top: 0;
+}
+
+.table tbody tr:hover > * {
+ background: var(--color-surface);
+}
+
+.table .right {
+ text-align: right;
+}
+
+.mono {
+ font: 0.86rem var(--font-mono);
+}
+
+.name,
+.name a {
+ color: var(--color-text);
+ font-weight: var(--weight-semibold) !important;
+}
+
+.name a:hover {
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.share {
+ align-items: center;
+ display: flex;
+ gap: var(--space-4);
+ min-width: 160px;
+}
+
+.share > span:first-child {
+ max-width: 200px;
+}
+
+@media (max-width: 640px) {
+ .table th:nth-child(3),
+ .table td:nth-child(3) {
+ display: none;
+ }
+
+ .table tbody th,
+ .table tbody td,
+ .table thead th {
+ padding-left: 6px;
+ padding-right: 6px;
+ }
+
+ .table td.rule {
+ min-width: 8rem;
+ white-space: normal;
+ }
+
+ .table .name {
+ white-space: normal;
+ }
+}
diff --git a/app/ui/ad-networks/network-ladder.tsx b/app/ui/ad-networks/network-ladder.tsx
new file mode 100644
index 00000000..73091c19
--- /dev/null
+++ b/app/ui/ad-networks/network-ladder.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import Link from "next/link";
+import { useState, type ReactNode } from "react";
+import { ToolbarSwitch } from "@/app/ui/table-toolbar";
+import { NotPublic } from "./details";
+import { AdSection } from "./section";
+import { formatCount } from "./site-format";
+import styles from "./niches-table.module.css";
+import ladder from "./network-ladder.module.css";
+
+export type LadderRow = {
+ name: string;
+ href?: string;
+ /** Sites in this niche that run the network. */
+ sites: number;
+ /** The joining rule in words, or null when the network doesn't publish one. */
+ rule: string | null;
+ /** Orders "Easiest to join": lower is easier, such as the traffic minimum. Null sorts last. */
+ ruleRank: number | null;
+};
+
+/**
+ * Every network that runs on a niche's sites, with its count, its share of the niche, and what it
+ * takes to join (mockup food page ladder). A switch orders by most used or easiest to join; it
+ * hides when no network has a rule. On phones the share column hides.
+ */
+export function NetworkLadder({ countLabel, description, id, rows, shareLabel, title, total }: {
+ /** Header of the count column, such as `food sites`. */
+ countLabel: string;
+ description?: ReactNode;
+ id: string;
+ rows: readonly LadderRow[];
+ /** Header of the share column, such as `share of food sites`. */
+ shareLabel: string;
+ title: ReactNode;
+ /** Sites in the niche, for the share. */
+ total: number;
+}) {
+ const [order, setOrder] = useState("used");
+ const hasRules = rows.some((row) => row.rule);
+ const canOrder = rows.some((row) => row.ruleRank !== null);
+ const top = Math.max(1, ...rows.map((row) => row.sites));
+ const list = [...rows].sort((a, b) => order === "join"
+ ? (a.ruleRank ?? Number.MAX_SAFE_INTEGER) - (b.ruleRank ?? Number.MAX_SAFE_INTEGER) || b.sites - a.sites
+ : b.sites - a.sites);
+ return (
+ : undefined}
+ description={description}
+ id={id}
+ title={title}
+ >
+
+ );
+}
+
+/**
+ * One section of an ad networks page (mockup `.nsec`): an h2 asked as a question, optional controls
+ * beside it, a one-line muted description, then the content. Sections that follow each other are
+ * split by a hairline.
+ */
+export function AdSection({ actions, children, description, id, title }: {
+ actions?: ReactNode;
+ children: ReactNode;
+ description?: ReactNode;
+ id: string;
+ title: ReactNode;
+}) {
+ return (
+
+ {actions ? :
{title}
}
+ {description ?
{description}
: null}
+ {children}
+
+ );
+}
+
+/** A small muted line under a block, such as where dates come from (mockup `.note`). */
+export function SectionNote({ children }: { children: ReactNode }) {
+ return
+ );
+}
+
+/** Muted words inside a site fact, such as `· 12 yrs`. */
+export function FactMuted({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+export type SiteNetworkRow = {
+ name: string;
+ href?: string;
+ logo: NetworkLogoFile | null;
+ /** What the site's ads.txt says about it, such as `Names it as the ad manager`. */
+ says: string;
+ /** Status in words, such as `Confirmed live` or `Not in Google's seller list`. */
+ status: string;
+ /** Confirmed statuses get a filled accent dot; others an empty one. */
+ confirmed: boolean;
+ /** Such as `Mar 2021`. */
+ since?: string;
+};
+
+/** The networks on one site, with what its ads.txt says and whether the network agrees (mockup `table.rel`). */
+export function SiteNetworksTable({ caption, rows }: { caption: string; rows: readonly SiteNetworkRow[] }) {
+ const since = rows.some((row) => row.since);
+ return (
+
+
+
{caption}
+
+
network
what its ads.txt says
status
{since ?
since
: null}
+
+
+ {rows.map((row) => (
+
+
{row.href ? {row.name} : {row.name}}
+
{row.says}
+
{row.status}
+ {since ?
{row.since ?? ""}
: null}
+
+ ))}
+
+
+
+ );
+}
+
+export type AdsTxtNumber = { label: string; value: ReactNode };
+
+/** Up to four ads.txt counts, such as lines, companies, direct, and resellers (mockup `.adstxt-nums`). */
+export function AdsTxtNumbers({ items }: { items: readonly AdsTxtNumber[] }) {
+ return (
+
+ {items.map((item) => (
+
{item.label}
{item.value}
+ ))}
+
+ );
+}
+
+/** A small mono subheading inside a block, such as `companies with the most lines` (mockup `.sub`). */
+export function BlockSubheading({ children }: { children: ReactNode }) {
+ return
{children}
;
+}
+
+/**
+ * The first lines of an ads.txt file as we read them (mockup `pre.adstxt`). Comments are dimmed,
+ * the MANAGERDOMAIN line is marked, and lines for `highlight` domains are brighter. `more` adds a
+ * closing "… N more lines" comment. `fileHref` adds a link to the full file.
+ */
+export function AdsTxtPreview({ fileHref, highlight = [], label, more, text }: {
+ fileHref?: string;
+ /** Seller domains to brighten, such as `mediavine.com`. */
+ highlight?: readonly string[];
+ label: string;
+ more?: number;
+ text: string;
+}) {
+ const bright = new Set(highlight.map((domain) => domain.toLowerCase()));
+ const lines = text.replace(/\r\n?/g, "\n").replace(/\n+$/, "").split("\n");
+ const line = (value: string) => {
+ const trimmed = value.trim();
+ if (trimmed.startsWith("#")) return {value};
+ if (/^managerdomain\s*=/i.test(trimmed)) return {value};
+ const domain = trimmed.split(",")[0]?.trim().toLowerCase();
+ if (domain && bright.has(domain)) return {value};
+ return value;
+ };
+ return (
+ <>
+
+ {body}
+
+ );
+}
diff --git a/app/ui/ad-networks/traffic-meter.module.css b/app/ui/ad-networks/traffic-meter.module.css
new file mode 100644
index 00000000..a4a3ec87
--- /dev/null
+++ b/app/ui/ad-networks/traffic-meter.module.css
@@ -0,0 +1,25 @@
+.wrap {
+ white-space: nowrap;
+}
+
+.meter {
+ display: inline-flex;
+ gap: 2px;
+ margin-right: var(--space-3);
+ vertical-align: -1px;
+}
+
+.meter i {
+ background: var(--color-surface-2);
+ border-radius: 1px;
+ height: 12px;
+ width: 5px;
+}
+
+.meter i[data-on] {
+ background: var(--color-accent-text);
+}
+
+.label {
+ color: var(--color-text-muted);
+}
diff --git a/app/ui/ad-networks/traffic-meter.tsx b/app/ui/ad-networks/traffic-meter.tsx
new file mode 100644
index 00000000..ed0e9c50
--- /dev/null
+++ b/app/ui/ad-networks/traffic-meter.tsx
@@ -0,0 +1,22 @@
+import { trafficStep, trafficText } from "./site-format";
+import styles from "./traffic-meter.module.css";
+
+/**
+ * Five small steps, filled by a site's place in Chrome's list of the most visited sites, then the
+ * group in muted words (mockup `.meter` + `top 10k`). Renders nothing for a missing group.
+ */
+export function TrafficMeter({ group, muted = true, showLabel = true }: {
+ group: string | null;
+ /** Muted group words, as in tables and lists. Set false inside a site fact, where the value reads plain. */
+ muted?: boolean;
+ showLabel?: boolean;
+}) {
+ if (!group) return null;
+ const step = trafficStep(group);
+ return (
+
+ {step ? {[1, 2, 3, 4, 5].map((index) => )} : null}
+ {showLabel ? {trafficText(group)} : null}
+
+ );
+}
diff --git a/app/ui/ad-networks/viz/age-strip.tsx b/app/ui/ad-networks/viz/age-strip.tsx
new file mode 100644
index 00000000..4cee455a
--- /dev/null
+++ b/app/ui/ad-networks/viz/age-strip.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { MeasuredChart, svgSize, useChartWidth } from "./chart-frame";
+import { formatNumber } from "./format";
+import { VizDataTable } from "./parts";
+import styles from "./viz.module.css";
+
+export type AgeBin = {
+ /** First registration year in the bin. */
+ year: number;
+ count: number;
+};
+
+export type AgeEnd = { year: number; domain?: string };
+
+const base = 66;
+
+/**
+ * When the domains were registered: a small histogram over a year line, the middle half as a
+ * band, the median as an accent line, and the oldest and newest domains at the ends (mockup
+ * domain age). Bins are evenly spaced, `binYears` apart.
+ */
+export function AgeStrip({ bins, binYears, caption, median, middleHalf, newest, oldest }: {
+ bins: readonly AgeBin[];
+ /** Years per bin. Defaults to the gap between the first two bins, or 1. */
+ binYears?: number;
+ caption: string;
+ /** Median registration year. */
+ median: number;
+ /** The 25th and 75th percentile years, drawn as a band. */
+ middleHalf?: readonly [number, number];
+ newest: AgeEnd;
+ oldest: AgeEnd;
+}) {
+ const { measured, ref, width } = useChartWidth(720);
+ const first = oldest.year - 1;
+ const last = Math.max(newest.year + 1, first + 2);
+ const x = (year: number) => ((year - first) / (last - first)) * (width - 8) + 4;
+ const top = Math.max(1, ...bins.map((bin) => bin.count));
+ const years = binYears ?? (bins.length > 1 ? Math.abs(bins[1].year - bins[0].year) : 1);
+ const barWidth = ((width - 8) / (last - first)) * years;
+ const tickStep = width < 420 ? 10 : 5;
+ const ticks: number[] = [];
+ for (let tick = Math.ceil((first + 1) / tickStep) * tickStep; tick < last; tick += tickStep) ticks.push(tick);
+ const two = width < 480;
+ const height = two ? 130 : 112;
+ const medianX = x(median);
+ const medianLeft = medianX + 110 > width;
+
+ return (
+ <>
+
+
+
+ [years > 1 ? `${bin.year} to ${bin.year + years - 1}` : String(bin.year), formatNumber(bin.count)])} />
+ >
+ );
+}
diff --git a/app/ui/ad-networks/viz/area-line.tsx b/app/ui/ad-networks/viz/area-line.tsx
new file mode 100644
index 00000000..0dcf252a
--- /dev/null
+++ b/app/ui/ad-networks/viz/area-line.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { InteractiveChart, svgSize, useChartWidth } from "./chart-frame";
+import { formatCompact, formatNumber, lineScale } from "./format";
+import { VizDataTable } from "./parts";
+import styles from "./viz.module.css";
+
+export type AreaLinePoint = {
+ /** Short axis label, such as `Nov`. */
+ label: string;
+ /** Full label in the hover note and data table, such as `Nov 2025`. */
+ fullLabel: string;
+ value: number;
+};
+
+const height = 170;
+const left = 44;
+const bottom = 22;
+const top = 8;
+const right = 10;
+
+/**
+ * One count over time as a filled line, ending in a dot (mockup sites over time). Hover, tap, or
+ * the arrow keys show the exact count and the change from the point before. `unit` follows the
+ * count in the note, such as `sites`.
+ */
+export function AreaLine({ caption, points, unit = "sites" }: { caption: string; points: readonly AreaLinePoint[]; unit?: string }) {
+ const { measured, ref, width } = useChartWidth(640);
+ const count = points.length;
+ const { high, low, ticks } = lineScale(points.map((point) => point.value), 3);
+ const plotWidth = Math.max(1, width - left - right);
+ const x = (index: number) => (count > 1 ? left + (index * plotWidth) / (count - 1) : left + plotWidth / 2);
+ const y = (value: number) => top + (1 - (value - low) / (high - low || 1)) * (height - top - bottom);
+ const path = points.map((point, index) => `${index ? "L" : "M"}${x(index).toFixed(1)} ${y(point.value).toFixed(1)}`).join(" ");
+ const step = Math.max(1, Math.ceil((count * 40) / plotWidth));
+ const last = points[count - 1];
+
+ return (
+ <>
+ (count > 1 ? Math.round((px - left) / (plotWidth / (count - 1))) : 0)}
+ label={caption}
+ measureRef={ref}
+ pointAt={(index) => {
+ const point = points[index];
+ const change = index ? point.value - points[index - 1].value : null;
+ return {
+ x: x(index),
+ y: y(point.value),
+ dot: true,
+ tip: (
+ <>
+ {point.fullLabel} · {formatNumber(point.value)} {unit}
+ {change === null ? null : <> · = 0 ? styles.up : undefined}>{change >= 0 ? "+" : "−"}{formatNumber(Math.abs(change))}>}
+ >
+ ),
+ };
+ }}
+ width={width}
+ >
+
+
+ [point.fullLabel, formatNumber(point.value)])} />
+ >
+ );
+}
diff --git a/app/ui/ad-networks/viz/bars.tsx b/app/ui/ad-networks/viz/bars.tsx
new file mode 100644
index 00000000..0f5b8e5e
--- /dev/null
+++ b/app/ui/ad-networks/viz/bars.tsx
@@ -0,0 +1,77 @@
+import Link from "next/link";
+import { Fragment, type ReactNode } from "react";
+import { clamp, formatNumber, toneColor, type VizTone } from "./format";
+import { VizLegend } from "./parts";
+import styles from "./viz.module.css";
+
+export type RankedBar = {
+ label: ReactNode;
+ value: number;
+ /** The number at the end of the row, such as `31%` or `7,852`. Defaults to the value. */
+ display?: ReactNode;
+ href?: string;
+ /** Marks the row the page is about, such as this network in a ranking. */
+ highlight?: boolean;
+};
+
+/**
+ * Ranked bars, label left and exact value right (mockup `dl.bars`). Bars scale to the largest value
+ * unless `max` is set. `tone="all"` fills every bar with the accent; `tone="highlight"` keeps the
+ * bars neutral and fills only highlighted rows. Reads as a description list, so it needs no table.
+ */
+export function RankedBars({ ariaLabel, max, rows, tone = "all" }: {
+ ariaLabel?: string;
+ max?: number;
+ rows: readonly RankedBar[];
+ tone?: "all" | "highlight";
+}) {
+ const top = max ?? Math.max(1, ...rows.map((row) => row.value));
+ return (
+
+ {rows.map((row, index) => (
+
+
{row.href ? {row.label} : row.label}
+
+
{row.display ?? formatNumber(row.value)}
+
+ ))}
+
+ );
+}
+
+export type StackPart = { label: ReactNode; value: number; tone: VizTone; /** Legend value, such as `88%`. */ display?: ReactNode };
+
+/**
+ * One thin bar split into parts of a whole, with a legend below (mockup `.stackbar`). Parts keep
+ * their order. Set `legend={false}` when the parts are named elsewhere.
+ */
+export function StackBar({ legend = true, parts }: { legend?: boolean; parts: readonly StackPart[] }) {
+ const shown = parts.filter((part) => part.value > 0);
+ return (
+ <>
+
+ {shown.map((part, index) => )}
+
+ {legend ? ({ label: part.label, tone: part.tone, value: part.display }))} /> : null}
+ >
+ );
+}
+
+export type Move = { from: ReactNode; to: ReactNode; fromHref?: string; toHref?: string; count: number };
+
+/** Switches between networks: `from → to`, a bar, and the count (mockup `dl.moves`). */
+export function MoveBars({ ariaLabel, moves }: { ariaLabel?: string; moves: readonly Move[] }) {
+ const top = Math.max(1, ...moves.map((move) => move.count));
+ const name = (label: ReactNode, href?: string) => href ? {label} : label;
+ return (
+
+ );
+}
+
+/** A chart box that only measures its width, for charts with no hover. */
+export function MeasuredChart({ children, measureRef }: { children: ReactNode; measureRef: RefObject }) {
+ return
{children}
;
+}
diff --git a/app/ui/ad-networks/viz/columns.tsx b/app/ui/ad-networks/viz/columns.tsx
new file mode 100644
index 00000000..25175568
--- /dev/null
+++ b/app/ui/ad-networks/viz/columns.tsx
@@ -0,0 +1,37 @@
+import { formatNumber, toneColor, type VizTone } from "./format";
+import { VizDataTable } from "./parts";
+import styles from "./viz.module.css";
+
+export type ColumnItem = {
+ label: string;
+ value: number;
+ /** The number above the column, such as `61%`. Defaults to the value. */
+ display?: string;
+ /** Defaults to the accent. Use `n2` for an "unknown" or "not ranked" group. */
+ tone?: VizTone;
+};
+
+/**
+ * One column per group with its value above and label below (mockup traffic, networks per site,
+ * and file size charts). Drawn in HTML, so text keeps its size at every width with no script.
+ */
+export function ColumnChart({ caption, height = 170, items }: { caption: string; height?: number; items: readonly ColumnItem[] }) {
+ const top = Math.max(1, ...items.map((item) => item.value));
+ const plot = height - 22 - 18;
+ return (
+
+ );
+}
diff --git a/app/ui/ad-networks/viz/dot-plot.tsx b/app/ui/ad-networks/viz/dot-plot.tsx
new file mode 100644
index 00000000..a1c72837
--- /dev/null
+++ b/app/ui/ad-networks/viz/dot-plot.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import { MeasuredChart, svgSize, useChartWidth } from "./chart-frame";
+import { niceScale } from "./format";
+import { VizDataTable } from "./parts";
+import styles from "./viz.module.css";
+
+export type DotRow = { label: string; shortLabel?: string; value: number };
+
+const labelWidth = 112;
+const rowHeight = 24;
+
+/**
+ * One dot per row on a shared scale from zero, with a stem and the value beside it (mockup median
+ * domain age). The first row's dot is accent, so pass the leader first. `unit` follows each value.
+ */
+export function DotPlot({ caption, rows, unit = "" }: { caption: string; rows: readonly DotRow[]; unit?: string }) {
+ const { measured, ref, width } = useChartWidth(360);
+ const { high, ticks } = niceScale(rows.map((row) => row.value), 3, true);
+ const x = (value: number) => labelWidth + (value / high) * (width - labelWidth - 40);
+ const height = rows.length * rowHeight + 20;
+ const suffix = unit ? ` ${unit}` : "";
+ return (
+ <>
+
+
+
+ [row.label, `${row.value}${suffix}`])} />
+ >
+ );
+}
diff --git a/app/ui/ad-networks/viz/format.ts b/app/ui/ad-networks/viz/format.ts
new file mode 100644
index 00000000..fc76c989
--- /dev/null
+++ b/app/ui/ad-networks/viz/format.ts
@@ -0,0 +1,106 @@
+// Shared by every ad network visual. Fixed en-US formats keep the server and browser output identical.
+
+/** The visual's fill steps: accent at three strengths, neutral at three, and the empty track. */
+export type VizTone = "ac" | "ac2" | "ac3" | "n1" | "n2" | "n3" | "track";
+
+/** The CSS color for a tone. The values live in `viz.module.css` under `.tokens`. */
+export const toneColor = (tone: VizTone) => (tone === "track" ? "var(--color-surface-2)" : `var(--viz-${tone})`);
+
+/** Strong accent fills take dark text in dark mode and white text in light mode. */
+export const onAccent = (tone: VizTone) => tone === "ac" || tone === "ac2";
+
+/** The default order of tones for parts of a whole, largest first. */
+export const partTones: readonly VizTone[] = ["ac", "ac2", "ac3", "n1", "n2", "n3"];
+
+export const formatNumber = (value: number) => value.toLocaleString("en-US");
+
+/** `7,000` reads `7k`, `7,500` reads `7.5k`, `1,200,000` reads `1.2M`. */
+export function formatCompact(value: number) {
+ const abs = Math.abs(value);
+ if (abs >= 1_000_000) return `${trim(value / 1_000_000)}M`;
+ if (abs >= 1_000) return `${trim(value / 1_000)}k`;
+ return trim(value);
+}
+
+const trim = (value: number) => String(Number(value.toFixed(1)));
+
+/**
+ * A percent from 0 to 100 for labels. Whole percents, except near the ends: a tiny nonzero value
+ * reads `<1%`, and only a complete 100 reads `100%` (99.96 reads `99.9%`). Round only for the
+ * label; keep raw values for sums, widths, and remainders.
+ */
+export function formatPercent(value: number) {
+ if (value > 0 && value < 1) return "<1%";
+ if (value >= 99.5 && value < 100) return `${Math.min(99.9, Math.floor(value * 10) / 10)}%`;
+ return `${Math.round(value)}%`;
+}
+
+/** A share from 0 to 1 as a percent label, with the same rules as `formatPercent`. */
+export const formatShare = (share: number) => formatPercent(share * 100);
+
+/** `+12`, `−4` (true minus sign), `0`. */
+export const formatSigned = (value: number) => (value > 0 ? `+${formatNumber(value)}` : value < 0 ? `−${formatNumber(-value)}` : "0");
+
+/** A round step that splits `span` into about `count` parts: 1, 2, 2.5, or 5 times a power of ten. */
+export function niceStep(span: number, count: number) {
+ const raw = Math.max(span, 1e-9) / Math.max(count, 1);
+ const power = 10 ** Math.floor(Math.log10(raw));
+ const unit = raw / power;
+ return (unit <= 1 ? 1 : unit <= 2 ? 2 : unit <= 2.5 ? 2.5 : unit <= 5 ? 5 : 10) * power;
+}
+
+/** Axis bounds and ticks around the data. `zero` keeps the axis starting at 0. */
+export function niceScale(values: readonly number[], count = 3, zero = false) {
+ const min = zero ? 0 : Math.min(...values);
+ const max = Math.max(...values, zero ? 1 : -Infinity);
+ const span = max - min || Math.abs(max) || 1;
+ const step = niceStep(span, count);
+ const low = zero ? 0 : Math.floor(min / step) * step;
+ const high = Math.max(Math.ceil(max / step) * step, low + step);
+ const ticks: number[] = [];
+ for (let tick = low; tick <= high + step / 2; tick += step) ticks.push(Number(tick.toFixed(6)));
+ return { low, high, ticks };
+}
+
+export const clamp = (value: number, low: number, high: number) => Math.min(Math.max(value, low), high);
+
+/** Charts with this many values or fewer carry a visually hidden data table. */
+export const DATA_TABLE_LIMIT = 60;
+
+/**
+ * Axis bounds for lines: some room below the lowest point, and the top snapped to the next round
+ * tick when the data comes close to it. Ticks are the round steps inside the bounds.
+ */
+export function lineScale(values: readonly number[], count = 3) {
+ const min = Math.min(...values);
+ const max = Math.max(...values);
+ const span = max - min || Math.abs(max) || 1;
+ const step = niceStep(span, count);
+ const ceiling = Math.ceil(max / step) * step;
+ const low = min - span * 0.12;
+ const high = ceiling - max < step * 0.35 ? ceiling : max + span * 0.12;
+ const ticks: number[] = [];
+ for (let tick = Math.ceil(low / step) * step; tick <= high + 1e-9; tick += step) if (tick > low) ticks.push(Number(tick.toFixed(6)));
+ return { low, high, ticks };
+}
+
+/**
+ * Where a value sits on fixed steps such as `0, 1k, 10k, 50k, 100k, 500k, 1M`: zero and below on
+ * the first step, others placed on a log scale between the two steps around them, so 25k sits
+ * a little past halfway from 10k to 50k. Values past the last step stay on it.
+ */
+export function logStepPosition(value: number, steps: readonly number[]) {
+ if (!steps.length) return 0;
+ if (value <= steps[0]) return 0;
+ for (let index = 1; index < steps.length; index += 1) {
+ if (value > steps[index]) continue;
+ const low = steps[index - 1];
+ const high = steps[index];
+ if (value === high) return index;
+ // A zero step has no log; values below the next step's half-way read as just past it.
+ const from = low > 0 ? Math.log10(low) : Math.log10(high) - 1;
+ const share = Math.max(0, (Math.log10(value) - from) / (Math.log10(high) - from));
+ return index - 1 + Math.min(1, share);
+ }
+ return steps.length - 1;
+}
diff --git a/app/ui/ad-networks/viz/heatmap.tsx b/app/ui/ad-networks/viz/heatmap.tsx
new file mode 100644
index 00000000..e1a4ebc6
--- /dev/null
+++ b/app/ui/ad-networks/viz/heatmap.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import Link from "next/link";
+import { useState, type CSSProperties } from "react";
+import styles from "./viz.module.css";
+
+export type HeatmapColumn = { key: string; label: string };
+export type HeatmapRow = { label: string; href?: string; values: readonly (number | null)[] };
+
+/**
+ * A grid of rows by columns where darker accent cells mean bigger values (mockup niche by network
+ * grid). Values are whole percents or counts, printed in each cell. Hovering a cell outlines its
+ * row and column. The first column stays put while the grid scrolls sideways on phones.
+ */
+export function Heatmap({ caption, columns, corner, rows, unitNote }: {
+ caption: string;
+ columns: readonly HeatmapColumn[];
+ /** Header of the row-name column, such as `niche`. */
+ corner: string;
+ rows: readonly HeatmapRow[];
+ /** A short mono line under the grid, such as `Numbers are % of the niche's sites.` */
+ unitNote?: string;
+}) {
+ const [cross, setCross] = useState<{ row: number; column: number } | null>(null);
+ const top = Math.max(1, ...rows.flatMap((row) => row.values.map((value) => value ?? 0)));
+ return (
+ <>
+
: null}
+ >
+ );
+}
diff --git a/app/ui/ad-networks/viz/history-gantt.tsx b/app/ui/ad-networks/viz/history-gantt.tsx
new file mode 100644
index 00000000..4bd7091a
--- /dev/null
+++ b/app/ui/ad-networks/viz/history-gantt.tsx
@@ -0,0 +1,68 @@
+import type { CSSProperties } from "react";
+import { clamp, onAccent, toneColor, type VizTone } from "./format";
+import styles from "./viz.module.css";
+
+export type GanttRow = {
+ label: string;
+ /** `YYYY-MM`, the month the network first appeared. */
+ from: string;
+ /** `YYYY-MM`, the last month it appeared, or null while it still runs. */
+ to: string | null;
+ tone: VizTone;
+ /** Added after the dates, such as `not confirmed`. */
+ note?: string;
+};
+
+const monthFormat = new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric", timeZone: "UTC" });
+const monthText = (value: string) => monthFormat.format(new Date(`${value}-01T00:00:00Z`));
+const monthLong = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", timeZone: "UTC" });
+const monthLongText = (value: string) => monthLong.format(new Date(`${value}-01T00:00:00Z`));
+// Where a label goes on wide screens, from the bar's share of the track. The track is at least
+// this wide above 640px; below that every label sits on its own line under its bar.
+const narrowestTrack = 560;
+const charWidth = 7.2;
+const barPadding = 16;
+
+const position = (value: string) => {
+ const [year, month] = value.split("-").map(Number);
+ return year + ((month || 1) - 1) / 12;
+};
+
+/**
+ * One bar per network from the month it first appeared to the month it last did, across whole
+ * years (mockup history timeline). Pass `endYear` as the current year so open bars run to "now".
+ * The whole chart is one image with a plain sentence for screen readers.
+ */
+export function HistoryGantt({ endYear, rows, startYear }: { endYear: number; rows: readonly GanttRow[]; startYear?: number }) {
+ const first = startYear ?? Math.min(endYear, ...rows.map((row) => Math.floor(position(row.from))));
+ const years = Array.from({ length: endYear - first + 1 }, (_, index) => first + index);
+ const span = years.length;
+ const at = (value: number) => clamp(((value - first) / span) * 100, 0, 100);
+ const summary = rows.map((row) => `${row.label} ${row.to === row.from ? `in ${monthLongText(row.from)}` : `from ${monthLongText(row.from)} to ${row.to ? monthLongText(row.to) : "now"}`}${row.note ? `, ${row.note}` : ""}.`).join(" ");
+ return (
+
+
{years.map((year) => {year})}
+ {rows.map((row) => {
+ const left = at(position(row.from));
+ const right = row.to ? at(position(row.to) + 1 / 12) : 100;
+ const width = Math.max(right - left, 1);
+ // One month reads as that month, not "Sep 2026 to Sep 2026".
+ const dates = row.to === row.from ? monthText(row.from) : `${monthText(row.from)} to ${row.to ? monthText(row.to) : "now"}`;
+ const text = `${dates}${row.note ? ` · ${row.note}` : ""}`;
+ const textShare = ((text.length * charWidth + barPadding) / narrowestTrack) * 100;
+ // Inside the bar when it fits, else after it, else before it. A label never gets cut short.
+ const place = width >= textShare ? "inside" : right + textShare <= 100 ? "after" : left >= textShare ? "before" : "inside";
+ const textStyle = { "--left": `${left}%`, "--right": `${right}%`, "--width": `${width}%` } as CSSProperties;
+ return (
+
+ {row.label}
+
+
+ {text}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/app/ui/ad-networks/viz/joined-left.tsx b/app/ui/ad-networks/viz/joined-left.tsx
new file mode 100644
index 00000000..8ce38810
--- /dev/null
+++ b/app/ui/ad-networks/viz/joined-left.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import { useState } from "react";
+import { InteractiveChart, svgSize, useChartWidth } from "./chart-frame";
+import { formatNumber, formatSigned } from "./format";
+import { VizDataTable, VizLegend } from "./parts";
+import styles from "./viz.module.css";
+
+export type JoinedLeftPoint = {
+ /** The day or week, such as `Oct 3` or `Week of Sep 28`. */
+ label: string;
+ joined: number;
+ left: number;
+ /** Position on the period's timeline, 0 for the first day or week. Needed with `slots`. */
+ slot?: number;
+};
+
+export type JoinedLeftPeriod = {
+ /** Stable key, such as `30`. */
+ key: string;
+ /** Switch label, such as `30 days`. */
+ label: string;
+ points: readonly JoinedLeftPoint[];
+ /** Words under the first bar, such as `30 days ago` or `13 weeks ago`. */
+ startLabel: string;
+ /** Words under the last bar. Defaults to `today`. */
+ endLabel?: string;
+ /**
+ * Days or weeks in the period. Set it when `points` holds only the days with changes: each bar
+ * then sits at its `slot`, and days with no changes stay empty instead of the bars stretching.
+ */
+ slots?: number;
+};
+
+const height = 120;
+const middle = 74;
+
+/**
+ * Sites that joined (accent, above the line) and left (neutral, below) per day or week, with a
+ * 30 days / 90 days switch and a legend of totals and the net change (mockup joined and left).
+ * Hover, tap, or the arrow keys show one bar pair. The switch hides when there is one period.
+ */
+export function JoinedLeftBars({ caption, periods }: { caption: string; periods: readonly JoinedLeftPeriod[] }) {
+ const [key, setKey] = useState(periods[0]?.key ?? "");
+ const [active, setActive] = useState(-1);
+ const { measured, ref, width } = useChartWidth(560);
+ const period = periods.find((item) => item.key === key) ?? periods[0];
+ if (!period) return null;
+
+ const { points } = period;
+ const count = points.length;
+ const slotted = Boolean(period.slots) && points.every((point) => point.slot !== undefined);
+ const barWidth = width / Math.max(1, slotted ? period.slots! : count);
+ const slotOf = (index: number) => (slotted ? points[index].slot! : index);
+ // With slots, a pointer over an empty day reads the nearest day with changes.
+ const indexAt = (px: number) => {
+ const slot = Math.floor(px / barWidth);
+ if (!slotted) return slot;
+ let best = 0;
+ points.forEach((point, index) => { if (Math.abs(point.slot! - slot) < Math.abs(points[best].slot! - slot)) best = index; });
+ return best;
+ };
+ const topMax = Math.max(1, ...points.map((point) => point.joined));
+ const bottomMax = Math.max(1, ...points.map((point) => point.left));
+ const scale = Math.min((middle - 4) / topMax, (height - 16 - middle - 2) / bottomMax);
+ const joined = points.reduce((sum, point) => sum + point.joined, 0);
+ const left = points.reduce((sum, point) => sum + point.left, 0);
+
+ return (
+ <>
+ {periods.length > 1 ? (
+
+ {periods.map((item) => (
+
+ ))}
+
+ ) : null}
+ ({
+ x: slotOf(index) * barWidth + barWidth / 2,
+ y: middle - points[index].joined * scale,
+ tip: <>{points[index].label} · {formatNumber(points[index].joined)} joined · {formatNumber(points[index].left)} left>,
+ })}
+ width={width}
+ >
+
+
+
+ [point.label, point.joined, point.left])} />
+ >
+ );
+}
diff --git a/app/ui/ad-networks/viz/lists.tsx b/app/ui/ad-networks/viz/lists.tsx
new file mode 100644
index 00000000..764b699a
--- /dev/null
+++ b/app/ui/ad-networks/viz/lists.tsx
@@ -0,0 +1,34 @@
+import Link from "next/link";
+import type { ReactNode } from "react";
+import styles from "./viz.module.css";
+
+export type NewestSite = { domain: string; href?: string; network: string; networkHref?: string; date: string };
+
+/** The newest sites with their network and the day we found them (mockup `table.newest`). */
+export function NewestList({ caption, sites }: { caption: string; sites: readonly NewestSite[] }) {
+ return (
+