Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Why Promovolve?

Digital advertising did not have to become what it became. The magazine era had already proven a different model: ads chosen to fit the publication, placed by people who cared about the page they appeared on, tolerated — often enjoyed — by readers because they belonged there. A travel magazine carried airline ads. A cooking magazine carried knife ads. Nobody was tracked.

The programmatic era — ads bought and sold by machines, in auctions run inside the milliseconds a page takes to load — traded that away for bidding over user profiles. The result is familiar: consent banners, ad blockers, fraud, clickbait inventory, and an arms race of bid optimizers playing each other instead of serving anyone. Publishers get a shrinking share of spend routed through a chain of intermediaries. Advertisers get impressions on pages they would never have chosen. Readers get followed around the internet by a pair of shoes.

Promovolve is an attempt to rebuild the magazine model with modern infrastructure. Its commitments, in order:

Target the page, not the person. An LLM reads the page a reader is actually looking at and classifies it into content categories. Campaigns target categories. There are no cookies, no user profiles, no device fingerprints, and nothing to consent to — the system never learns who the reader is.

Let the reader steer. A Promovolve ad is a small magazine: it sits folded in the page, and expands into a full-screen spread only when tapped. Readers can fold a corner — a dog-ear — to bookmark an ad they want back. The bookmark lives in their own browser. Re-encounters with a bookmarked ad are free for the advertiser and invisible to the learning system: a remembered ad is a gift, not a billable event.

Make honesty the best bid. The auction is second-price and quality-adjusted, so shading a bid never helps and a creative readers actually engage with beats one that merely pays more. Promovolve ships no campaign-side bid optimizer because the mechanism leaves nothing to optimize.

Give the publisher the controls. Every creative passes through the publisher’s approval queue before it can serve on their site. Floors are optimized on the publisher’s behalf, per content category, by measuring served revenue — not by an exchange with its own agenda.

Show the work. The platform is open source, and this book explains how it actually operates — including the parts that are deliberately simple and the ideas that were tried and dropped. Where the text names a class, the class exists; where a mechanism was removed, the book says so.

The next chapter defines the trade itself — publisher, advertiser, impression, auction — from zero, for readers who have never bought or sold an ad; skip it if you have. The chapters after it tell the story once, quickly, through the eyes of a page and a reader — and then take each mechanism apart: the ad format, the creative pipeline, classification, the auction, approval, serve-time selection, pricing, pacing, floors, and the cluster underneath it all. The last chapter measures the design against conventional ad tech, difference by difference.

The Deal Behind Every Ad

If you have never bought or sold an ad, this chapter is for you. It defines every word the trade uses, because the rest of the book uses them freely. If you’ve run campaigns or monetized a site before, skip ahead — nothing here will surprise you.

Two strangers with matching problems

A publisher owns a place readers visit — a travel blog, a recipe site, a local news page. The readers cost money to serve and the writing costs money to produce, but the readers don’t pay. What the publisher has is attention.

An advertiser has the opposite problem: a pilates studio, an online shop, an airline — something to offer and nobody looking. What the advertiser lacks is attention.

Advertising is the trade between them. The publisher sets aside rectangles on the page — slots — and rents them out. The advertiser fills them with a message. Everything else in this book is machinery for making that trade fair, fast, and worth everyone’s while.

The units of the trade

One person seeing one ad, once, is an impression. It is the atom of the business: it’s what gets counted, priced, and paid for.

A single impression is worth a fraction of a cent, so prices are quoted per thousand: CPM, cost per mille. “A $5 CPM” means five dollars for a thousand views. When this book says an advertiser “bids $5,” that is the number being bid.

The ad itself — the images, the words, the layout, the thing a reader actually sees — is called a creative. It’s an odd noun, but it is the industry word and the book uses it constantly: one advertiser might run several creatives, to see which one readers like.

A campaign is the advertiser’s standing order: these creatives, this daily budget, this CPM bid, aimed at this kind of content. Campaigns are what compete for slots.

Why an auction

At any moment, many campaigns want the same slot. Someone has to decide who gets it and at what price, millions of times a day, with no humans negotiating. The answer nearly everywhere — including here — is an auction: every eligible campaign names its price, and the machinery picks a winner.

Auctions come in two flavors, and the difference matters enough that this book has a chapter about it. In a first-price auction the winner pays what they bid — which punishes honesty, because bidding your true maximum means overpaying whenever the competition is weak. So first-price bidders hire software to guess the minimum winning bid, a practice called bid shading. In a second-price auction the winner pays what was needed to win — just above the runner-up’s bid. Overbidding no longer costs you anything, so the safe strategy is simply to bid what the impression is worth to you. The price the winner actually pays, in either flavor, is called the clearing price.

The publisher, for their part, can set a floor — a minimum price below which the slot simply doesn’t sell. Floors protect against thin competition: with only one bidder, “just above the runner-up” would otherwise mean nearly free. The catch is that a floor set too high scares off every bidder and the slot earns nothing at all — an unsold slot is called unfilled, and the share of slots that do serve an ad is the publisher’s fill rate. Choosing a floor is a genuine dilemma, and this book devotes a chapter to measuring the answer instead of guessing it.

Where the money goes

The advertiser’s daily budget caps what a campaign may spend per day — and spending it well means making it last the whole day, not just the morning (that problem is called pacing). Each impression’s clearing price is subtracted from the budget, the platform keeps a small percentage as its margin, and the rest is the publisher’s earnings. The daily bookkeeping that turns a stream of impressions into money owed and money earned is settlement.

That’s the entire vocabulary: two parties, a slot, an impression, a price per thousand, an auction, a floor, a budget, and the split. The rest of this book is how Promovolve runs this old trade differently — starting with the story of one page, one reader, and one ad.

A Page, a Reader, an Ad

Everything in this book happens in the few seconds this chapter describes. Read it once for the shape; the rest of the book explains each step.

A publisher signs up

Yuki runs a small travel site. She registers it with Promovolve, an operator approves the site request, and she proves she controls the domain — a token file at /.well-known/promovolve.txt, or a DNS TXT record if her host won’t serve files. From that moment, only pages on her verified host can request ads under her site ID; anyone who copies her embed code onto another domain gets a 403.

She drops one script tag into her template. Her slots are just divs with dimensions. There is nothing else to configure — no ad server UI, no line items (the hand-negotiated delivery contracts of traditional ad servers), no size negotiations.

An advertiser signs up

Kenta owns a pilates studio. He has no design team and no creative agency — he has a landing page. He gives Promovolve the URL, a daily budget, and a CPM bid — the price he’s willing to pay per thousand views of his ad. The platform’s pipeline reads his landing page in a real browser, extracts its images, copy, and colors, has an LLM rewrite the copy into a three-page magazine narrative, renders it, and shows him the result. He picks the categories his campaign should appear against — Fitness, Wellness — and launches.

A new article meets its first reader

Yuki publishes an article about a hot-spring town at 6 a.m. At 6:14, the first reader arrives. The ad tag on the page asks for ads — and the server has never seen this URL. It answers with empty slots and a hint: send me the text. The tag extracts the article’s text in the reader’s own browser and posts it up. An LLM classifies it — Travel, Asia Travel — and the result is stored. That one reader saw no ads; every reader after them will.

Classification is good for 48 hours. If the article still has traffic after that, the next visit re-sends the text and the clock resets. Pages nobody reads simply fall out of the system.

The auction runs — before anyone else arrives

With categories in hand, the site’s auctioneer collects bids from every campaign registered against Travel and its neighbors. Kenta’s campaign isn’t a travel campaign, so it sits this one out; an airline and a luggage brand bid. The auctioneer doesn’t pick one winner. It orders all eligible creatives — each campaign’s best foot forward first — and caches the whole pool next to the page. The real selection happens later, per impression. Every five minutes, and whenever a campaign changes, the auction quietly re-runs.

A reader gets an ad

The second reader loads the page. The ad tag sends one request listing every slot on the page. The server checks the host, checks the classification is fresh, pulls the cached candidate pool from a local replica, and lets its pacing controller decide whether this impression should even be spent — a campaign’s budget must last the whole day, not just the morning.

Then it samples. Each candidate’s click and dog-ear history is a pair of Beta distributions; each candidate draws a plausible engagement rate, multiplies by a dampened function of its bid, and the highest draw wins the slot — one campaign at most per page. The winner’s budget is reserved, the price is set by the runner-up rather than the winner’s own bid, and the response carries the creative and its tracking URLs.

The reader answers back

The ad sits collapsed in the page, magazine-cover small. The reader taps; it expands into a swipeable spread — cover, story, call to action. That expand is the click in this format — the reader chose to open the magazine. If the reader wants the ad back later, they fold its corner — a dog-ear, the fold event, the strongest quality signal the system has — and their own browser remembers it. Next time they meet that advertiser on any page of the site, the bookmarked creative is served, free, with no auction and no learning: the system refuses to bill or optimize a moment the reader chose.

If instead the reader ignores the ad, that’s data too. Within an hour, the sampling distributions have shifted; a creative nobody engages loses its share of impressions to one readers actually open.

The day ends

At midnight UTC, spend counters roll over, the pacing controller notes how today went and adjusts its aggressiveness for tomorrow, and the site’s learned traffic shape absorbs today’s rhythm — a 20% nudge toward what actually happened. Settlement writes the day’s ledger: gross spend, platform margin, publisher earnings, one idempotent row per advertiser–campaign–site.

Nobody was tracked. Nothing about the reader left their browser except a click. And every mechanism in this story is a chapter in this book.

The Magazine Format and the Dog-Ear

Promovolve serves exactly one kind of ad: an expandable magazine. The format is not a stylistic preference — half the system’s design depends on it.

Collapsed and expanded

In the page, the creative — the ad artifact itself, the thing readers see — is collapsed: a cover — image, headline, an advertiser tag — occupying whatever rectangle the publisher provided. Tapped, it expands into a full-screen overlay the reader swipes through like a small magazine: a cover page, a story page, and a call-to-action page. Collapse it and the page is exactly as it was.

The three-page narrative is fixed by design. It gives the LLM copywriter a stable dramatic structure (hook, substance, ask), gives the designer a stable layout target, and gives readers a consistent gesture vocabulary. The renderer is a self-contained script that mounts the creative inside Shadow DOM, so publisher CSS cannot leak in and the ad cannot leak out.

The event vocabulary follows the format. Opening the magazine fires the click — in this format a click is the expand, a reader choosing to spend attention rather than to leave the page. Tapping any page of the spread navigates to the landing page (the CTA event). And a fold is the dog-ear gesture below — bookmarking the ad — which is the strongest signal of all: serve-time selection learns from clicks and folds, with folds weighted double.

Folds cost the advertiser nothing. There is no cost-per-fold billing anywhere in the system — a bookmark volunteered by the reader is not a billable event.

Fluid, not fixed-size

A creative is a layout, not a bitmap. The same creative renders into a wide leaderboard strip, a boxy rectangle, or a tall half-page rail — the shapes ad slots traditionally come in; the renderer reads the slot’s geometry at mount time and reflows — container queries, not server-side variants. Publishers offer whatever slot shapes suit their design; advertisers maintain one creative instead of a matrix of sizes. This is what lets a pilates studio with no design staff participate at all.

The dog-ear

The corner of every folded creative can itself be folded down — a dog-ear, the gesture readers already use on paper. A dog-eared ad is a bookmark:

  • It lives in the reader’s browser. The pin is stored in IndexedDB, keyed by slot, and presented back to the server with each ad request. The server signs fold state into a stateless token; it never stores who bookmarked what, because it never knows who anyone is.
  • It wins the slot. At serve time, a valid pin bypasses the auction, scoring, pacing, and budget entirely: the bookmarked creative simply serves. On pages where the pinned slot doesn’t exist, the pinned advertiser is excluded from the normal auction site-wide — the system must never burn a reader’s saved ad as an ordinary impression, and must never chase the reader with the same advertiser’s other creatives.
  • It is free and unlearned. Pinned re-encounters are counted in their own dogeared_* counters and excluded from spend, from CTR learning, and from every optimization loop. A bookmark is the reader’s choice; the moment the system monetizes or learns from it, it stops being one.
  • It heals itself. If the advertiser leaves the site, the campaign ends, or the creative is revoked, the serve response tells the client which pins are stale, and the client deletes them from IndexedDB. A transient lookup failure never deletes a pin — only a confirmed “this creative is gone” does.

One consequence surprises people operating the system: if you dog-ear an ad on your own site while testing, that advertiser stops appearing in normal rotation for you. The system is working as designed — your browser asked it to.

From Landing Page to Creative

Most small advertisers have exactly one designed artifact: their landing page. Promovolve treats it as the source of truth for the ad — the campaign input is a URL, not a zip of banner assets.

The pipeline

  1. Extraction. A Playwright browser (the LPWorker pool, running on cluster nodes with the crawler role) loads the landing page and extracts its raw material: headline candidates, body copy, images, and a brand kit — dominant background color, text color, and a palette of up to six swatches ordered by how much painted area each covers. The brand kit is measured from the rendered DOM, not guessed by a model, so the ad inherits the landing page’s actual look.

  2. Rewriting. An LLM turns the extracted copy into the three-page magazine narrative — cover hook, story, call to action — under hard constraints: claims must be grounded in the landing page’s own text, and verbatim-sensitive details (prices, phone numbers) are carried through unchanged rather than paraphrased.

  3. Rendering and verification. The creative is rendered headlessly and uploaded to object storage (Cloudflare R2, served through the CDN). An LLM then verifies it against two questions: does the ad actually match its declared content categories, and is it brand-safe — no adult, violent, or hateful content? The check reads the authored text where it exists and falls back to the rendered image only when it doesn’t; the verdict updates the creative’s verification and safety status and auto-derives its target categories.

Color is code, not model output

Text colors are never chosen by the LLM. A deterministic pipeline picks them: luminance decides a dark-on-light or light-on-dark palette, every combination is checked against the WCAG AA contrast ratio (4.5:1), and any brand-kit color that fails contrast is replaced by a compliant fallback. The prompt hard-codes the allowed hex values so the model cannot invent an unreadable one. If a creative looks wrong, the bug is in the contrast pipeline — a place you can set a breakpoint — not in a model’s mood.

The designer

Advertisers can hand-tune the generated creative in an in-browser designer. Its editing model took several iterations to get honest, and the invariants are worth stating because they define what a “creative” is:

  • One image per page, defined by the expanded view. The image shown on page one of the full-screen spread is the page’s image everywhere — the folded cover and every slot size derive from it. There is no per-size image pinning; replacing the image in the expanded view replaces it everywhere, always.
  • Text and color sync across sizes by default. Each text field carries a per-size “synced across all sizes” setting. Unsynced, edits stay local to that slot size; re-ticking sync adopts the current text as the shared value. Headline, body, and page-background colors additionally sync across the three pages, each with its own toggle (page background defaults to synced).
  • Deletion is one-sourced. Deleting a field from the expanded view removes it from every size that carries it — and the confirmation dialog counts exactly which sizes those are, rather than claiming “everywhere.”

The folded and expanded views are two projections of one layout document, so nothing the advertiser does can make the cover advertise a different product than the magazine inside it.

How a Page Gets Ads

Promovolve targets content, so before a page can carry ads, the system has to know what the page is about. The interesting decision is when that happens: not on a crawl schedule, but the first time a reader shows up.

On-demand, reader-triggered

There is no crawler walking publisher sites. (One existed early on; it spent its nightly budget re-reading pages nobody visited, and was deleted. The crawler cluster role survives only as the host for landing-page analysis workers.) Instead:

  1. The ad tag requests ads for a URL the server has never classified. The response carries empty slots and a freshness token of zero — send text.
  2. The tag extracts the page’s readable text in the browser (bounded at 8,000 characters) and posts it to /v1/classify-page, which answers 202 Accepted immediately. Classification never blocks serving.
  3. The site’s entity classifies the text with an LLM (Gemini, currently gemini-2.5-flash) into IAB Content Taxonomy 3.0 categories — the top three, with confidence scores. Campaigns register their demand — the categories they want to buy — against the same taxonomy, so matching is a direct category lookup with ancestor expansion (a page about Baseball matches a campaign targeting Sports); there is no intermediate mapping layer.

The page that triggers classification gets no ads; every subsequent reader does. Pages classify exactly when readers prove they exist, and LLM cost is bounded by distinct fresh URLs, not by traffic: a single-flight guard in the site entity collapses a story’s burst of first visitors into one classification call.

Freshness, not publish dates

A classification is valid for the site’s classification freshness window — 48 hours by default. This is a TTL on the classification, not a check on the article’s publication date; nothing in the system reads publish dates at all.

Every serve response carries reclassifyInMs, the token that drives the loop. While positive, the ad tag sends nothing. When it lapses, the next visit re-submits the text, and a fresh classification opens a fresh window. The consequences:

  • Evergreen content keeps serving. A three-year-old article with live readers re-classifies every 48 hours forever.
  • Dead pages expire. A page whose traffic stops falls out of every cache — the auctioneer prunes classifications past the window every five minutes. State is bounded by what readers actually visit.
  • Content drift is caught. An edited article gets re-read within a window.

Built to survive restarts and outages

Classifications are persisted in the site’s durable entity and replayed to the auction layer three ways: on entity recovery, when a fresh auctioneer announces itself (a restarted auctioneer starts with an empty page cache and must be re-taught), and on a five-minute refresh tick. The replay is idempotent — a same-or-older timestamp is ignored — so the paths can overlap harmlessly. A cluster restart therefore heals itself: within moments the auctioneers relearn every fresh page and re-run their auctions.

The LLM call itself is wrapped in a circuit breaker (five consecutive failures open it for thirty seconds) and a token-bucket rate limiter sized to the API tier. A failed classification just releases the single-flight slot; the next reader retries. The serving path never waits on a model.

The Periodic Auction

Real-time bidding — conventional ad tech’s model — runs an auction per impression and gives each one the few milliseconds a page load can spare. Promovolve inverts this: auctions run ahead of impressions — when a page is classified, on a periodic tick, and when the world changes — and their results are cached. Serving then reads the cache. The auction can afford to be thoughtful because nobody is waiting on it.

What an auction produces

For each page and slot, the site’s AuctioneerEntity assembles a candidate pool, not a winner:

  1. Demand lookup. The page’s categories (with ancestor expansion) are resolved against CategoryBidderEntity actors — the registry of which campaigns bid on which categories. A CPM threshold keeps only competitive bids, and each category contributes at most its top ~50 campaigns by CPM.
  2. Bid collection. Each eligible campaign’s entity is asked for its creatives and current bid. Campaigns apply their own filters here — a site allowlist, if the advertiser restricted where they appear.
  3. Ordering, no cap. Candidates are deduplicated by creative, sorted by CPM (publisher-approved creatives win ties), and reordered so each campaign’s best creative comes first. The full pool from the eligible campaigns is kept — no further cut at ordering time. Serve-time selection needs losers to learn from; an auction that discarded them would silently disable exploration.
  4. Caching. The pool is written to the ServeIndex — a replicated, locally-readable cache described in The Cluster — with a TTL.

Campaigns whose creatives the publisher hasn’t approved yet still bid: that is how a creative reaches the approval queue. But pending demand cannot serve and is invisible to floor optimization — an unapproved bid must not teach the market anything.

When auctions run

  • On classification — a page’s first auction follows its first classification within moments.
  • On a timer — every site re-evaluates its fresh pages periodically (the deployment runs a 5-minute interval; the code default is 30). The timer is a backstop; the event-driven paths below do most of the work.
  • On events, debounced — campaign created, paused, or re-targeted; creative approved, rejected, or flagged; bids changed; floors moved. Each triggers re-evaluation of the affected pages on a one-second debounce.
  • On boot — a restarted auctioneer is re-taught its classifications by the site entity and immediately kicks a re-auction, so a cluster restart converges without waiting for the timer.

Budget exhaustion is not removal

When a campaign exhausts its daily budget, its ServeIndex entries are not deleted — deletion would also discard the publisher-approval status attached to them, forcing every creative back through the approval queue at midnight. Instead the entries’ TTL is refreshed past the day rollover and the serve path simply refuses to select over-budget campaigns. At rollover the budget resets and the creatives resume instantly.

The same principle governs every eviction decision in the system: removal events are deliberate and scoped (an advertiser suspended, a campaign leaving a site takes its reader pins with it), while temporary conditions mark rather than delete. Most historical serving bugs traced back to violations of exactly this rule.

The Publisher’s Gate

A magazine editor decides which ads appear next to their writing. Promovolve keeps that power with the publisher: no creative serves on a site until that site’s publisher approves it, and the publisher can change their mind at any time.

How creatives reach the queue

There is no submission form. When a campaign wins auctions on a site, its unapproved creatives are queued for that site’s approval automatically — the auction is the submission. The publisher’s dashboard shows each pending creative exactly as readers would see it: the folded cover, and the full expanded magazine, rendered live. The queue updates over server-sent events as new candidates arrive.

Approval is per-creative, per-site. Approving a creative for one site says nothing about any other.

The publisher’s verbs

  • Approve — the creative may serve. It moves into the live pool immediately; no re-auction needed.
  • Reject / Flag — the creative is blocked from bidding on this site. The block is a membership entry in a deletable filter (a cuckoo filter, replicated cluster-wide), so it’s enforced at bid time, cheaply, on every auction. Unflagging deletes the entry and the creative may compete again. The block is reversible by design — “flagged” means until unflagged, not forever.
  • Revoke — the strongest undo of an approval: the creative stops serving and returns to pending. It keeps bidding (that’s how it re-enters the queue), but it cannot serve until re-approved. Any reader dog-ears pointing at it are reported stale and cleaned from readers’ browsers.
  • Block a domain — publishers can also block by landing-page domain, removing every creative that links there, from any advertiser.

Trust, once earned

Reviewing every creative from an advertiser you’ve already vetted is busywork, so a publisher can opt in to auto-approval, per site, off by default. The reasoning behind its shape:

  • A manual approval is a statement of trust — not just in one image, but in the campaign behind it and the brand it links to. So each manual approval records two trust anchors for the site: the campaign, and the landing page’s registrable domain (shop.acme.com and www.acme.com are the same brand; two tenants of a shared hosting platform are not). With the toggle on, a new creative matching either anchor skips the queue and serves immediately, marked as auto-approved so the publisher can always audit what the feature did on their behalf.
  • Trust is consumed, never chained. Auto-approvals mint no anchors of their own — otherwise one approval could snowball across an advertiser’s whole portfolio via shared domains. Only a human clicking Approve widens trust.
  • A reject is evidence the inference failed. Auto-approval is a bet that past approval predicts future approval; rejecting, flagging, or revoking any creative from a trusted campaign or domain settles that bet the other way and withdraws the anchors. Siblings return to the manual queue. Anchors can also be removed individually from the dashboard.
  • Off by default, deliberately. Approved demand is what teaches floor prices, so widening what gets approved automatically changes a site’s economics. That choice belongs to the publisher, not a default. For the same reason there is no retroactive grant: approvals made before the feature existed mint no anchors, because the rejections that would have balanced them were never recorded.

Turning the toggle off pauses auto-approval without deleting the trust list; turning it back on restores it.

The lifecycle rule that took a production incident to learn

Pausing a campaign and deleting its approvals are different acts, and conflating them once wiped publishers’ approval queues during a routine deploy. The settled rule:

  • An explicit pause by the advertiser revokes the campaign’s approvals on the site — pausing is leaving; on resume, every creative starts over as pending. This is a product decision: a publisher who approved a campaign in March shouldn’t discover it silently resumed in July. (Trust anchors are the publisher’s own state and survive the pause — on a site that opted into auto-approval, resume re-approves on the next auction, because there the publisher has said they want exactly that.)
  • Everything else keeps approvals — budget exhaustion, category re-registration churn during deploys, entity restarts, re-verification. None of these are the advertiser leaving, so none of them may touch the publisher’s decisions.

Approval state, like everything reader- and publisher-facing in the system, errs toward preservation: statuses are marked, not deleted, unless a human explicitly chose otherwise.

Serve Time: Thompson Sampling

The auction cached a pool of candidates. Now a reader is here, and the system must pick — quickly, per impression, for every slot on the page at once. This is where Promovolve learns.

One request per page

The ad tag sends a single batch request listing every slot. The site’s AdServer entity checks, in order: the requesting host is the site’s verified host (403 otherwise); the page’s classification is inside the freshness window (204 otherwise); reader dog-ears are honored first (a valid pin takes its slot and skips everything below); and the pacing gate — see Budget Pacing — decides whether this impression should be spent at all.

What remains is selection over the candidate pool.

Score by sampling, not by averages

Each creative carries a rolling 60-minute window of impressions, clicks, and folds, bucketed by minute. (CTR below is click-through rate: of the readers who saw this creative, the share who opened it.) From it, two Beta distributions:

sampledCTR  ~ Beta(clicks + 1,  impressions − clicks + 1)
sampledFold ~ Beta(folds + 1,   impressions − folds  + 1)

Every request, every candidate draws from its distributions — it does not use its mean. The score:

engagement = sampledCTR + 2.0 × sampledFold + newcomerBonus
score      = engagement × CPM^α

Sampling is the whole trick (this is Thompson Sampling). A creative with 1,000 impressions draws values tightly around its true rate; a creative with 9 impressions draws wildly. Uncertain creatives therefore sometimes draw high and win — exploration — in exact proportion to how uncertain they are. No exploration schedule, no epsilon, no phases: confidence itself allocates the experiments, and as data accumulates the draws narrow and the best creative simply wins most often.

Clicks here are magazine-opens — in this format the click is the expand. Folds are dog-ears, weighted 2× because a reader bookmarking the ad is a stronger signal than one merely opening it.

The exponent α is the publisher’s one tuning knob (bidWeight): 0.5 by default, so a $10 bid beats a $1 bid by ~3.2×, not 10× — price matters, but a creative readers engage with can beat a richer one that they ignore. Publishers wanting discovery set 0.3; wanting revenue, 0.7.

Cold start, inside the same formula

The cold start is the ranking problem’s awkward first day: how do you score an ad with no history? New creatives get two helps, both expressed as scores — there is no separate cold-start code path, round-robin, or forced serving:

  • Zero impressions: the CTR draw is replaced by the creative’s category affinity score (how well its category matched the page at auction time) plus uniform noise of ±0.15, and the fold draw comes from a Beta(1, 3) prior. A relevance-informed guess instead of a coin flip.
  • First 50 impressions: an additive newcomer bonus starting at +0.5 and decaying linearly to zero — a guaranteed runway against confident incumbents, gone by the time the creative has real data.

Selection stays a single argmax — take the highest score, nothing else — at every lifecycle stage.

Filling the page

Slots are assigned greedily, largest first, each taking the remaining candidate with the highest sampled score — under two hard rules: a creative appears at most once per page, and a campaign appears at most once per page. A page plastered with one advertiser is bad for the reader, the publisher, and the advertiser; the constraint is enforced at assignment time, where it can’t be gamed.

The winner’s budget is reserved before the response is sent, and the impression is recorded server-side at selection — billing does not depend on a tracking pixel surviving the reader’s browser. Clicks (expands), folds (dog-ears), and CTA events arrive later through tracking endpoints and update the Beta windows. Dog-eared re-encounters update none of this: they live in separate counters, excluded from learning, spend, and reporting’s primary metrics.

What the Winner Pays

Promovolve is second-price at heart: the winner pays what was needed to win, not what they offered. But the score that wins is engagement × CPM^α, not a bare bid — so the clearing price must be quality-adjusted too.

Sample for allocation, price on means

Selection uses sampled (noisy) values on purpose; pricing must not. A price that depended on a random draw would make identical impressions cost different amounts. So the system allocates on samples, prices on posterior means: after the winner is chosen, the runner-up’s score is recomputed from mean engagement rates, and the winner pays the minimum CPM at which it still would have won:

clearingCPM = (runnerUpScore / winnerEngagement)^(1/α)

clamped between the slot’s floor and the winner’s own bid. Intuition: invert the scoring formula and ask, “with your engagement rate, what’s the cheapest bid that still beats the next-best candidate?”

Two properties fall out:

  • Quality is a discount. A creative readers engage with needs a lower CPM to hold its rank, so it pays less than a mediocre creative bidding identically. Advertisers improve their price by improving their ad.
  • Bidding is honest. Raising your bid above what’s needed doesn’t raise your price (the runner-up sets it); lowering it only risks losing. There is no bid-shading strategy to compute (shading: bidding below your true value to dodge overpaying — the daily homework of first-price auctions), which is why Promovolve ships no campaign-side bid optimizer — the mechanism leaves nothing for one to do.

The runner-up is taken from the winner’s own content category, so the price reflects real competition for this kind of page, not an accidental cross-category comparison.

Edge cases

  • Exploration usually prices at the floor. A zero-history winner is priced by the same mean formula, using its cold-prior engagement — the category affinity, the fold prior, and the newcomer bonus. Because the bonus inflates its engagement, the inverted price typically clamps down to the floor; a cold winner facing a strong same-category runner-up can still clear above it.
  • No runner-up → floor. A lone candidate pays the floor. (What stops floors from collapsing in a one-bidder market is the floor optimizer — see Floor Optimization — which pegs the floor to a lone bidder’s bid.)
  • Pinned re-encounters are free. A dog-eared creative serving to the reader who bookmarked it clears at zero. The reader’s memory is not inventory.

Spend is recorded at the clearing price

Budget reservation, pacing, and the ledger all use the cleared price, not the bid. A campaign bidding $8 into thin competition might spend $2.10 per thousand — its budget lasts proportionally longer, and the advertiser’s reports show the price they actually paid. Every spend event flows through buffered, deduplicated, at-least-once recording into a double-entry ledger in micro-dollars (millionths of a dollar — integer arithmetic, so the books never accumulate rounding drift); settlement splits gross into platform margin (a percentage set in basis points — hundredths of a percent — that can change on a dated schedule) and publisher earnings, one idempotent row per advertiser–campaign–site–day.

Budget Pacing

A daily budget spent by 9 a.m. serves the advertiser badly — the evening audience never sees the campaign, and the morning’s impressions were bought in a rush. Pacing’s job is to make spend track the day, and its design principle is the control engineer’s, not the machine learner’s: a simple feedback loop, self-tuned by observation.

The gate

Pacing acts at serve time as a probabilistic gate ahead of selection: each request, each campaign is throttled with some probability; a throttled campaign sits the impression out. The probability comes from a PI controller — the thermostat’s algorithm: correct in proportion to today’s error and to its accumulated history — watching the spend ratio:

error      = smoothed(actualSpend / expectedSpend) − 1
throttle   = clamp(Kp·error + Ki·integral, 0, 0.99)

Overspending pushes the throttle up; underspending lets it fall. The controller is deliberately asymmetric — over-pacing errors are amplified by a multiplier (base 2×) because overspend is irrecoverable while underspend can be caught up later.

The refinements are where the production behavior lives, and each earns its place:

  • Self-tuning. The overpace multiplier adjusts itself (between 1.5× and 5×) from a rolling window of recent spend ratios: persistent overspend makes the controller more aggressive, well-paced days relax it, and the learned value carries across days. Fixed gains, self-tuned application.
  • Oscillation damping. If the spend ratio starts see-sawing, smoothing increases until it settles — a controller that hunts is worse than one that lags.
  • Anti-windup. The integral term is clamped and decays, so a long morning of underspend can’t bank an afternoon of unthrottled spending.
  • Grace periods. A fresh day (or a just-started campaign) gets a short window with gentle throttling — both a minimum time and a minimum request count, because low-traffic sites may take an hour to see ten requests. Without the request-count condition, small sites were throttled on the basis of no evidence at all.
  • Cross-day learning. A campaign that exhausted its budget early today starts tomorrow with a boosted multiplier hint.

Expected spend follows the traffic, not the clock

expectedSpend is the subtle half. Linear pacing (X% of budget by X% of day) over-throttles the morning peak and sets impossible targets overnight. Promovolve instead learns each site’s traffic shape: 24 hourly volumes, learned separately for weekdays and weekends, from the arrival times of ad requests themselves.

The learning is deliberately conservative. A brand-new site bootstraps a shape during its first day with a fast intra-day estimator; from then on — permanently — the shape changes only at the UTC-midnight rollover, when today’s observed distribution blends 20/80 into the stored shape. Restarts restore the persisted shapes (both day types) and re-enter the same regime; snapshots are written hourly, at rollover, and on shutdown. Ten similar days converge the shape; one anomalous day barely dents it.

Shapes are learn-only. There is no API or dashboard field to set one, deliberately: a hand-authored shape encodes intuition, not measurement, and a wrong shape is worse than none — flat degrades gracefully to exactly linear pacing, while wrong throttles the real peak. Publishers can see the learned shapes (they’re charted on the site’s observations page, and exported in site stats) but not edit them.

The shape serves pacing twice: its cumulative curve becomes the expected spend fraction the PI controller tracks, and its per-hour relative volume scales the impression-rate target, interpolated across hour boundaries so rates ramp instead of stepping.

What isn’t here

There is no per-impression bid adjustment, no predicted-win-rate model, and no volatility-scheduled gain table (one was designed; production only ever needed the self-tuned controller, so it was deleted). Pacing is a feedback loop you can reason about with a napkin — which is exactly why it can be trusted to gate every impression the platform serves.

Floor Optimization

A floor price protects a publisher from selling attention below its worth — set too low, second-price clearing grinds toward zero in thin competition; set too high, fill rate — the share of slots that actually serve an ad — collapses. Publishers shouldn’t have to guess this number, so Promovolve measures it.

Sweep, don’t learn

The FloorSweepOptimizer is deliberately not reinforcement learning — an earlier RL approach (value estimates over discretized floor levels) was built, evaluated, and dropped: with pacing and Thompson Sampling already adapting around every floor change, credit assignment was hopeless, and the agent mostly learned noise. What replaced it is controlled measurement:

  1. Sweep. Generate candidate floors across the plausible range (bounded below by observed rejected bids, above by the best observed bid). Hold each candidate for a fixed number of auction ticks, measuring served revenue — actual post-pacing, post-selection earnings, not theoretical clearing prices. Revenue is the only honest objective; anything upstream of it can be gamed by the very systems the floor interacts with.
  2. Exploit. Take the argmax — the floor that earned the most — and hold it for a longer exploitation period. Ties within tolerance resolve to the higher floor — the same revenue on fewer impressions, and more robust if the dominant bidder’s value drifts up. The optimizer only fails open to the lowest floor when evidence is missing, and a minimum-impressions guard keeps a lucky low-traffic candidate from winning on a handful of data points.
  3. Repeat. Markets drift; the cycle re-runs continuously.

Per-category floors

A single site-wide floor has a monopoly pathology: one rich category (say, Finance demand at $12) sets a floor that locks out every other category’s demand entirely. Floors therefore run per content category, each category sweeping independently; categories without enough data fall back to the site-level floor. The pathological case that motivated this — a lone high bidder pushing the site floor above everyone else — now prices one category at $12 while Travel still clears at $3.

Two guardrails matter more than the sweep itself:

  • Only approved demand teaches floors. Pending creatives bid (to reach the approval queue) but are invisible to the optimizer — otherwise an unapproved, possibly rejected campaign could inflate a floor that outlives it.
  • A lone bidder pegs the floor to its bid. With one approved bidder there is no second price and nothing to sweep; the floor snaps to the bid (and instantly back down when the bidder leaves — validated live in both directions). And with zero approved demand the floor collapses to the minimum immediately: a floor with nobody to price against is pure fill-rate damage.

One honest caveat: in a perfectly homogeneous market — every bidder at the same CPM — every floor below the common bid earns identical revenue, and the optimizer settles anywhere on that plateau. That is correct behavior, and a reminder of what this component is: not intelligence, just a well-designed experiment that never stops running.

The Cluster

Everything so far — classification, auctions, selection, pacing, floors — is stateful, per-site work. The architecture question is where that state lives. Promovolve’s answer: in actors, one per real-world thing, each the single writer of its own state.

Entities

The system runs on Apache Pekko cluster sharding. Each entity is an actor, addressed by ID, living on exactly one node at a time; the shard coordinator places and moves them as nodes join and leave.

EntityOne perOwns
SiteEntitysiteverification, slots, classifications
AdServersiteserving, pacing, creative stats, floors
AuctioneerEntitysiteauctions, page cache, approval queue feed
CampaignEntitycampaignbids, creatives, budget
AdvertiserEntityadvertisercampaigns, approvals, daily budget
CategoryBidderEntitycategory × sharddemand registry

A site’s entire serving path — pacing state, Thompson windows, floor sweeps — is a single actor processing one message at a time. That is a design position, not an accident: no locks, no cache-coherence protocol, no read-modify-write races, because there is nothing concurrent to race with. The trade is a throughput ceiling per site — one actor’s mailbox — and the position holds because a single actor comfortably absorbs a site doing millions of impressions a day. Sites that ever exceed one actor’s capacity are a scaling problem the design would meet with sharding within the site, not by making the state stateless.

Nodes carry roles — api (HTTP), entity (sharded actors), singleton (cluster-wide directories), and crawler, which despite its historical name now exists to host the landing-page analysis workers. Blocking work (Playwright, LLM calls, JDBC) runs on dedicated dispatchers so an entity under load cannot starve the serving path.

The ServeIndex: replicated reads, single-writer writes

Serving reads must not cross the network. The candidate pools live in the ServeIndex, built on Pekko Distributed Data (CRDTs): every node holds a full replica, so a serve-time lookup is a local map read. Entries are spread across 32 named maps by a hash of site|slot — replication works map-by-map, and one giant map would re-gossip everything on every change.

Consistency is asymmetric on purpose. Writes acknowledge locally (WriteLocal) and gossip outward — auction results becoming visible on other nodes tens of milliseconds late is harmless. The exception is whole-key removal, which uses majority writes with retries: a removal that loses a gossip race resurrects deleted candidates, and (worse) removal tombstones are the one place where divergent replicas can disagree destructively. Fast paths are eventually consistent; destructive paths pay for certainty.

Replicas are durable — DData persists designated keys to a local LMDB store — so a restarting node recovers its cache from disk instead of re-gossiping the world. Combined with the boot-time classification replay (the site entity re-teaches a fresh auctioneer, idempotently), a full cluster restart self-heals in about a minute: the “post-restart dark window” during which slots briefly serve empty, then refill on their own.

Rules learned the hard way

Cross-node messages are Jackson-CBOR-serialized by marker trait, and the discipline is strict because failures are silent: a reply payload missing the marker simply vanishes at the network boundary. Bare tuples and other unregistered shapes are banned from cross-node protocols. Durable state evolves by field aliasing, never by renaming persisted fields outright. And any state a future needs must be delivered back to the actor as a message — completing a Future inline against actor state is the concurrency bug the whole architecture exists to prevent.

The platform around the core — dashboards, approval queues, billing, member management — is a separate Go service rendering server-side templates (no SPA framework), talking to the core over HTTP and to Postgres for projections and the ledger. Authentication is passkey-only. But every serving decision described in this book happens inside the actors.

Against the Grain of Ad Tech

Promovolve diverges from conventional programmatic advertising on almost every axis. This chapter is the honest scorecard — including what the conventional stack does better.

The differences

Per-impression auctions → periodic auctions. RTB gives every impression its own auction under a ~100ms deadline, which forces every participant to precompute everything and answer with a cache anyway. Promovolve moves the auction off the hot path entirely: it runs at classification time and on change events, and serving is a local cache read plus a Beta draw. The trade: candidate pools can be minutes stale. Event-driven re-auctions with a one-second debounce keep the staleness bound tight where it matters.

User targeting → content targeting. No cookies, no profiles, no consent apparatus, because there is nothing to consent to. The trade is real: no retargeting, no frequency-managed brand campaigns across sites, no audience segments. Promovolve’s bet is that page context — read by an LLM rather than keyword-matched — recovers most of the relevance at none of the privacy cost, and the dog-ear gives readers the retargeting control that ad tech gives advertisers.

Highest-bid-wins → sampled quality scores. A traditional exchange never learns whether the winner was any good. Promovolve’s selection is a learning system: engagement posteriors sharpen with every impression, new creatives get exploration in proportion to their uncertainty, and the formula (engagement × CPM^α) lets a well-made ad beat a well-funded one.

Bid landscapes → nothing to optimize. DSPs — demand-side platforms, the bidding software advertisers hire to play the exchanges — exist substantially to shade bids. Quality-adjusted second pricing removes the incentive: your price is set by the runner-up and discounted by your own engagement rate. Promovolve ships no bid optimizer, and that absence is a feature of the mechanism, not a missing roadmap item.

Fixed IAB sizes → fluid creatives. One layout reflows into any slot. Small advertisers produce one creative from one landing page URL; the pipeline (browser extraction → LLM copywriting → deterministic contrast-checked styling → vision-model verification) replaces the design team they don’t have.

Exchange-side yield tools → publisher-side everything. Approval queues, domain blocks, per-category measured floors — the controls sit with the publisher, and the floor optimizer’s objective is the publisher’s served revenue, measured, not modeled.

What the conventional stack still does better

Honesty requires the other column. Programmatic ad tech delivers scale and liquidity Promovolve does not: demand from thousands of buyers through open exchange protocols, remnant fill (a buyer of last resort for any unsold slot anywhere), and cross-site campaign tooling — reach, frequency capping (limiting how often one person sees an ad), brand-lift measurement — that a content-targeted, single-platform system structurally cannot offer. RTB’s per-impression auction also prices this reader now — worth real money for performance advertisers — where Promovolve deliberately prices only this page.

Promovolve is not trying to beat the exchange at the exchange’s game. It is a different deal: for publishers who want curated, reader-respecting monetization with controls they actually hold, and for advertisers — small ones especially — who want their landing page turned into a magazine ad and priced by an auction that doesn’t require a quant team to enter honestly.

The system described in this book is small enough to read, honest enough to audit, and open source so you can do both.

Glossary

The trade’s vocabulary, and this book’s, in one place. Terms specific to Promovolve are marked ⊛.

Advertiser — the party with something to sell and no attention; buys impressions. In this book, usually a small business with a landing page.

Bid shading — bidding below your true value to avoid overpaying in a first-price auction. An entire software industry exists to do this; second-price auctions make it pointless.

Campaign — an advertiser’s standing order: creatives + daily budget + CPM bid + targeted content categories.

Clearing price — what the winner actually pays, as opposed to what they bid. In Promovolve: quality-adjusted second price, clamped between the floor and the winner’s bid.

CPM — cost per mille: the price of a thousand impressions. The unit all bids and prices are quoted in.

Creative — the ad itself: images, copy, layout; the artifact readers see. One campaign may run several. In Promovolve a creative is a fluid layout, not a fixed-size image.

CTA — call to action; the “come do the thing” page or button. ⊛ In Promovolve’s event vocabulary, the CTA event is a tap-through from the expanded magazine to the advertiser’s landing page.

CTR — click-through rate: of the readers who saw a creative, the share who clicked it. ⊛ Here a “click” is opening the magazine — the expand.

Demand — the buying side: which campaigns want which pages, at what price. “Registering demand” = a campaign declaring the categories it bids on.

Dog-ear (fold) ⊛ — the reader’s corner-fold gesture that bookmarks an ad in their own browser. Free for the advertiser, invisible to learning, strongest quality signal the system has.

Fill rate — the share of ad slots that actually serve an ad. An unfilled slot earns nothing.

First-price / second-price auction — pay-what-you-bid versus pay-what-was-needed-to-win (just above the runner-up). Second price makes honest bidding the safe strategy.

Floor (price) — the publisher’s minimum acceptable price for a slot. Too low invites near-free clearing in thin competition; too high kills fill. ⊛ Promovolve measures floors per content category instead of asking the publisher to guess.

Impression — one person seeing one ad once. The atom of the business: counted, priced, paid for.

Inventory — publisher-side supply: the slots (attention) a publisher has to sell.

Landing page — the advertiser’s own destination page, where a reader lands after tapping through. ⊛ In Promovolve it is also the source the ad is generated from.

Line item — in traditional ad servers, a hand-configured delivery contract (dates, sizes, targeting, price). Promovolve has none.

Margin — the platform’s percentage cut of gross spend; the rest is publisher earnings.

Pacing — making a daily budget last the whole day instead of being spent by 9 a.m.

Publisher — the party with readers and costs; sells impressions. In this book, usually a small content site.

RTB (real-time bidding) — conventional ad tech’s model: an auction per impression, run in the ~100ms a page takes to load. ⊛ Promovolve replaces it with periodic auctions whose results are cached.

Settlement — the bookkeeping that turns a day of impressions into money owed (advertiser), money earned (publisher), and margin (platform).

Slot — a rectangle on the page the publisher rents out. In Promovolve, a div with dimensions.