SubtleAdssponsorship placement for apps
Sign in

Designing a campaign type

A campaign type is the one document that defines what a sponsor can buy from your app — price, delivery rules, ad slots, the creative you need from them, any custom fields. Everything an advertiser sees — the booking form, the quote, a rejection message — is generated from it. Worth sketching before you register.

Already registered? The guided editor walks the same decisions below, one step at a time, and validates as you go.

What it actually is

A campaign type is flat, not nested into sections: name, description (optional), a pricing block, a delivery block, one or more slots, and optional assets, fields, and validationRules. There is no separate “basics” object — name and description sit at the top level alongside everything else.

Start with what’s sellable, not the price

Figure these out in order: where can an ad appear (slots), what do you need uploaded for it (assets), what other information rides along (fields) — then price it.

A slot is a named place an ad can appear. Most slots point at exactly one asset via assetKey; several slots can share one asset (DrivingTest’s three highway billboards below all point at the same texture). Leave assetKey off altogether for a text-only slot — a placement you compose yourself from the sponsor’s name, brand colour, one-line pitch and button label, with no file involved. It sells, serves and bills like any other slot, and the sponsor is never asked to upload anything for it. Use that rather than declaring an asset you know you won’t render: the booking form asks for every asset you declare, so a placeholder gets collected from a paying sponsor and then never drawn. An optional role (intro, ambient, mark, outro, interstitial) is a platform-wide vocabulary for cross-app booking — skip it if you don’t need it, a slot sells and serves fine without one. A slot can also point at a preview surface — a screenshot of the real place with the ad’s footprint pinned onto it, so a sponsor sees their own creative composited in before they pay. That’s set up separately, once you’re registered, at /app/surfaces.

An asset declares the shape you want — aspect ratio, target width or duration, file formats, a size ceiling — never “whatever the sponsor happens to upload.” The ingest pipeline conforms their single high-resolution master to exactly that shape. It only rejects a file when no safe transform exists: the source is smaller than the minSourceWidth you set (upscaling looks bad), or a contain crop would need to stretch the aspect more than 3× to fit (a logo would come out unrecognisable). It never rejects a file merely for being “the wrong size.”

A field is anything besides a file: boolean, select, text, link, number, date, or color. A color field can declare contrastFloors — a WCAG-style legibility check run at submission, before moderation, before payment, so an illegible colour is rejected with a reason instead of silently booked and never delivered right.

Picking a pricing model

Every campaign type uses exactly one. All money is minor units (cents) under the hood; advertisers only ever see dollars.

If…UseBecause
a guaranteed, date-bound placement, sold regardless of trafficflat / cpdSame idea; cpd prices it per day, for variable-length flights.
the standard usage-based ad unitcpmBilled per confirmed impression, shown to sponsors as “$4 CPM.”
a click is a real, instrumented action in your appcpcSkip it if nothing actually happens on click — it’s not a proxy for attention.
one price per genuine session the ad was actually shown incpsImmune to multi-slot inflation by construction — five slots filled in one session still bill once.
selling a guaranteed share of decisions, several sponsors running at oncesovA delivery guarantee (“50% of matching requests for two weeks”), not a consumption unit — and it can’t pair with an exclusive slot (below).

cpm/cpc/cps are metered: a budget depletes as it delivers, and serving stops when it’s spent. flat/cpd/sov are reserved: a fixed price buys a fixed window, regardless of volume. Pacing (below) only means anything for metered models — there’s nothing to pace when the price doesn’t depend on delivery.

Shared vs. exclusive

A slot’s exclusivity defaults to shared — several campaigns can win it over time. Make a slot exclusive_per_slot only when that’s a real product fact (“this card back can only show one logo at a time”), not a pricing tactic to charge more — the seeded “Exclusive Route Takeover” fixture (packages/db/src/seed.ts) is a real example: flat pricing, $500 per 7–30 day flight, one slot, all to one sponsor. maxConsecutiveRenewals (default 2) and bookingHorizonDays (default 90) are the guardrails that stop “exclusive sponsorship” from becoming “permanent lockout” — the same sponsor can’t just keep renewing forever, and can’t book more than 90 days out.

The schema enforces two combinations you can’t ship, not just advises against:

Two more delivery knobs worth knowing early: dedupeWindowSec (default 30) collapses confirmations for the same campaign and session inside that window into one billable impression — it’s about the same moment. frequencyCap (e.g. { count: 4, perHours: 24 }) is about repetition across time instead — how often the same viewer can see it at all.

Four worked examples

3D game-world billboard

This is SubtleAds’ own trial app, not a hypothetical — the real spec lives at packages/db/src/seed.ts and is driven live by apps/demo-drivingtest/src/main.ts.

Three billboards along a driving-test route, sharing one 16:9 image asset (video optional). cpm at $4.00, $100 minimum budget, geo multipliers for US/CA traffic and a volume discount past $5,000 spend. shared delivery, dedupeWindowSec: 30, frequencyCap: { count: 4, perHours: 24 } so the same driver doesn’t see it more than 4 times a day.

Mobile app banner + interstitial

Two distinct slots, priced differently because they cost the sponsor different amounts of attention. A persistent bottom banner: cpm, shared, role: ambient, a gentle frequency cap — it’s always there, easy to ignore. A between-screens interstitial: also cpm but at a materially higher rate, and a tighter frequencyCap since it’s more intrusive and needs to earn its interruption. Both use cropStrategy: "smart" on the image asset so a phone-shaped crop doesn’t cut through a logo.

Newsletter / content sponsorship

flat or cpd, not cpm — there’s no reliable per-open confirm signal for an email, so a metered model would be pricing something you can’t actually count. One slot, role: mark or outro, a small 3:1 logo asset with cropStrategy: "contain" so it letterboxes instead of cropping through a wordmark, plus a link field and a short text field (capped with maxLength) for a one-line pitch. This is also the honest case for exclusive_per_slot: “presented by one sponsor per issue” is a real product fact for a newsletter, not a pricing trick.

Podcast / audio pre-roll

An audio asset (maxDurationSec, loudness normalized to a declared loudnessLufs so no sponsor blows out someone’s headphones). flat or cpd per episode is the honest default — a play count isn’t a confirmed listen. cps is the harder-but-more-honest option if your player can genuinely instrument “playback continued past the spot.” Two slots via role: intro (pre-roll) and role: interstitial (mid-roll) sharing one asset, so one sponsor can buy both without uploading twice.

Rules the schema enforces for you

Not advice — the platform stops you from shipping these:

Where this becomes real

Start a campaign type once you’ve registered — the guided editor walks these same decisions and validates against this exact schema at every step. Or skip straight to “Whole document as JSON” in the sidebar, paste a complete document in one shot, and click “Apply to all sections” then “Create campaign type.” Either way validates against the identical rules, so nothing you build can drift from what’s described here.

For your coding agent: hand it this same framework and worked examples, written as copy-pasteable JSON — it can sketch a complete document for you to drop straight into “Whole document as JSON” above.

See also: wiring the SDK, once you have something to sell · browse what other apps already sell, for inspiration.