Appearance
ADR-0004 · Advertising & sponsored placements platform scope
STALE — read current architecture state first (2026-08-08)
The inert promo_slot seam is unchanged and still fails closed forever. But a first-party vendor offer is a DIFFERENT block type with a live resolver (ADR-0025 D1) — conflating them would either kill the campaign engine or open this ad seam before compliance_profile exists. industries.compliance_profile is now designed (ADR-0020), which is the column this ADR needs.
Status: 🟡 Proposed, amended 2026-08-04 · Depends on: ADR-0003 · Drives: —
Amendment — 2026-08-04: the seam is defined and shipped, but it is INERT — this ADR's open questions are still open
The Setu Card + Catalog foundation design (2026-08-04) needed the sponsored block type this ADR depends on to exist in the manifest schema now, without waiting for either open question below to resolve. Three things changed, none of which answer this ADR's actual questions:
- The block ships as
promo_slot, notsponsored(naming carried over from ADR-0003'scustom-block cut —promo_slotreads more accurately as "a place where a promotion COULD render" than "sponsored," which implies content already exists). Manifest shape:{ type: 'promo_slot', slotId }. Templates declare where a promo could go; they never declare what — that stays the resolver's job, unchanged from this ADR's original recommendation. resolvePromo(slotId, ctx)always returnsnulland fails closed forever, by design, not by omission. The reason is a verified gap, not a timing choice:compliance_profile/ad_restricteddoes not exist onbusiness_domains— no column, confirmed against the live schema — and ADR-0009 itself defers it while stating plainly thatad_restricteddomains must suppress ad slots regardless of this ADR's configuration. Shipping any live promo resolution before that flag exists would put a doctor, CA or loan-agent's card at risk of carrying an ad it cannot legally carry, with the platform having no mechanism to stop it. The resolver returningnullunconditionally is the only correct behaviour until the flag lands — this is a compliance floor, not a feature gate, and it does not lapse when Option A ships.- A caching constraint is now recorded that any future implementation of either Option A or B must satisfy, and did not exist in the original analysis: the public card is edge-cached per-slug (Cloudflare, >90% target). A promo resolved per visitor makes the card response vary per request and defeats that cache entirely. Any real implementation must resolve per-slug (a campaign assigned to a vendor/domain, cacheable with the rest of the card) or resolve client-side, after paint, outside the cached HTML. This constrains the eventual resolver's design; it does not change this ADR's Options A/B/C analysis below, which is untouched and still fully open.
Nothing in this amendment answers Open Question 1 or 2 below. Whether QRSETU wants ads on cards at all, and whether it fits the brand, remains exactly as unresolved as it was — the amendment exists solely so the schema does not need a second migration whenever that question is eventually answered.
Context
The AdManager review confirmed AdManager and manifest.js are properly wired (QRPlatform.CAMPAIGNS/removeCampaign/setCampaignEnabled), and that a template-embedded ad placement concept genuinely exists (servicecard.footer, among 7 slots). But three real gaps surfaced:
servicecard.footeris defined but has no consumer —ServiceCard.dc.html(confirmed read directly) never callsgetPromo/QRPlatformanywhere. Supply-side tooling exists; demand-side doesn't.- A self-serve external advertiser portal is UI scaffolding only — its own code marks the invite action
(demo). A real version implies a third external-facing product surface (advertiser auth, ad-spend billing) with no architecture behind it. - Advertiser-uploaded creative content has no sanitization/validation model — same risk class as the Templates Custom HTML finding, but from a less-trusted external party once (2) is real.
This also connects to an already-logged, still-open trust question: ad-serving inside a paying merchant's own dashboard needs explicit product sign-off (confirmed hardcoded "Sponsored" banner in mobile-console/Home.dc.html) — a strategy decision, not a design detail.
Options considered
A — Internal-only ad ops
Admin curates and targets all campaigns directly; no external advertiser accounts, auth, or self-managed budgets. This matches what's actually implemented today — the self-serve portion is demo-only UI.
- For: smallest trust surface; ships now with the least new architecture (just the
sponsoredblock type from ADR-0003 plus fixing the orphaned slot). - Against: doesn't realize the prototype's full stated ambition of a self-serve advertiser marketplace.
B — Full self-serve advertiser marketplace
External advertisers get their own accounts, manage budget/creative themselves, and pay QRSETU directly for ad spend. This is what the AdManager prototype's copy describes.
- For: a genuine second revenue line if there's real advertiser demand.
- Against: a third external-facing product surface layered on top of QRSETU's existing merchant/individual audiences — needs its own auth model, its own billing (separate from ADR-0002's merchant subscription billing), its own creative-moderation pipeline, and resolves the still-open "ad-serving inside a paying merchant's dashboard needs sign-off" question implicitly by assuming yes.
C — Staged: ship A now, gate B on validated demand
Formalize A as the real, current scope — relabel the self-serve section honestly rather than leaving it reading as a working feature. Treat B as a distinct future initiative, opened only once: ad revenue is confirmed to clear an ROI bar against the trust cost of showing ads inside a paying customer's own tool, and inventory-fill (who is actually buying) has a real answer.
- For: doesn't compound one unresolved bet (self-serve marketplace) on top of another already-open one (should merchant-dashboard ad-serving exist at all).
- Against: slower path to a potential second revenue line, if there is real advertiser demand today.
Recommendation
C. Standing up a full self-serve advertiser marketplace before the more basic "should QRSETU sell ads inside a paying merchant's own dashboard at all" question is answered would be building a second unresolved product bet on top of a first one. Formalize A as current scope now (a design-fixable Claude Designs prompt: relabel the self-serve section as "internal ops, self-serve planned" rather than implying it's live), and log B as its own future ADR, opened only when the dashboard-ad-serving trust question is resolved and there's real advertiser demand to size against.
Regardless of A vs B timing, design the sponsored block/slot schema from ADR-0003 generically enough that both internal-curated campaigns (A, now) and eventual self-serve campaigns (B, later) can populate the same render contract — don't hardcode "admin-only" into the rendering path even while keeping the buy-side admin-only for now. This avoids a second migration later just to let a different actor create the same kind of campaign row.
Consequences
- Fixes the orphaned
servicecard.footerslot by givingServiceCard.dc.html(or its future template equivalent, per ADR-0003) a realsponsoredblock that consumes it — a design-fixable prompt already exists for this in the admin-panel review. - Advertiser creative sanitization only needs solving for real once B is greenlit — until then, the existing human approval gate ("the ops team reviews and approves") is the acting mitigation, since the only creative submitter is internal ops itself under option A.
- The dashboard-ad-serving trust question stays open and blocking for B specifically; it does not block A, since A is simply formalizing what's already shipped.
Open questions for product
- Does QRSETU want a second, ads-based revenue line at all, and does showing sponsored placements inside a paying merchant's own dashboard fit the brand (a "premium, ad-free-feeling" bar was raised during the Service Card review)?
- If yes to (1): what would need to be true for B to clear an ROI bar over its added trust/build cost?
Related
- Tracker: "servicecard.footer slot defined but not consumed," "Self-serve advertiser portal is UI scaffolding only," "Advertiser-uploaded creative content has no sanitization model," "Ad-serving inside a paying merchant's own dashboard needs explicit product sign-off"
- Admin panel review — Ad Manager
- ADR-0003 (the
sponsored/promo_slotblock type this depends on) - QRS-348 (inert promo seam, shipped 2026-08-04) · QRS-086 (
ad_restricted/compliance_profilenot built)