How Doozer builds and mails postcards. The Factory assembles accurate cards deterministically (real screenshot/code/QR, no per-card AI cost); the AI Theme Studio designs vision-gated backgrounds; campaigns give each card a purpose + audience. Claim Your Site is the first campaign.
v2.2.381 — the Factory is now the default, accurate path. Generating the
whole card with AI every time was expensive AND inaccurate (it approximated the
claim code, invented the short URL, hallucinated the domain in the screenshot's
address bar). The Postcard Factory (includes/postcard-factory.php) flips
it: AI designs backgrounds/layouts; every ACTUAL card is composed
deterministically from REAL values and flattened to a print-ready 1800x1200 PNG
via Cloudflare Browser Rendering (postcardFactoryFlattenpostshtml+
viewport to the same CF endpoint used for screenshots). Real screenshot in a
browser chrome showing the ACTUAL Doozer domain, real minted claim code, real
doozer.link short URL, a real scannable QR, real logo, Machi copy. No per-card AI
cost, nothing to proofread. postcardFactoryCompose($bizId,$opts) files it in
the same review queue (no gate — values are deterministic). Themes: paper / navy.
Admin: AI Postcards page "Build card" panel; Machi defaults to engine='factory'.
v2.2.395 — Multi-campaign system. Postcards are no longer claim-only. A
campaign = purpose + audience + copy/offer (postcard_campaigns,
includes/postcard-campaigns.php). The seeded, locked claim_your_site
campaign (audience=unclaimed) IS the old behavior, now named. campaign already
threaded through postcard_designs/thanksio_sends/claim_codes/machi_touches as a
string; this formalizes the keys. Audiences (hard-coded): all / unclaimed
(admin_email empty) / claimed (admin_email set) / premium (owner lb_user on a
premium-class tier), each + niche (primary_niche) + location (state_code/city);
base = live Doozer site + mailable address. `postcardCampaignAudienceWhere/Count/
Sample. Purpose seeds generation: postcardCampaignThemePromptSeed` feeds the
Theme Studio background prompt (postcardThemeCreateoptcampaign);
postcardCampaignMessage resolves the BACK copy (claim → warm/leads/curious
variants; else message_template with tokens {name}{city}{code}{offer_code}
{short_url}). Admin: Campaigns cards + Create/Edit modal (live audience count),
Campaign selector on Factory + Studio, review queue pagination + finder (search +
campaign filter). dz campaign do=list|show|save. NEXT phase: campaign-specific
FRONT copy + audience-gated sending (don't mail a claimed business a claim card).
v2.2.392 — Two-score gate + back copy + Thanks.io levers. The theme gate
(postcardThemeGateCard) now returns TWO independent real scores because there is
no in-admin way to tweak a theme after the fact: accuracy (every required
element present, uncropped, legible — an explicit checklist is passed to the vision
model) and design (aesthetics + theme match only). A theme goes ACTIVE only
when BOTH clearpostcard_theme.gate_pass_score(default 78); elsedraft. Stored
inpostcard_themes.design_score/accuracy_score(idempotent ALTER); shown in
the Studio result and gallery tiles. Card template fix: .left scrim is full grid
height (grid-template-rows:100%) so the "doozer." wordmark is always inside the
legibility box. Back-of-card copy (thanksIoClaimMessageVariants in
thanksio.php): warm / leads / curious variants, personalized with the town + a
typed doozer.link fallback, no em-dashes. Thanks.io best practice: the FRONT
is the Factory image; the BACK ismessage+qrcode_url+ return address. Turn
on thanksio.handwriting_style_id (handwritten back lifts response), set
thanksio.size (4x6 default; 6x9/6x11 stand out but need a matching front aspect),
thanksio.message_variant. Rules that win direct mail: personal, specific to them,
ONE clear action (scan), readable in three seconds, honest, warmly signed.
v2.2.390 — AI Theme Studio. The untrustworthy full-AI card painter is
repurposed into a theme/background designer only; the Factory still does the real
assembly.includes/postcard-theme-ai.php:postcardThemeCreate($style,$opts)
takes a plain-words style ("beach vacation", "Thanksgiving warm orange",
"Bronx blue/white pinstripe") + optionalbusiness_id/match_sitefor niche
style cues, builds a LAYOUT-AWARE background prompt (calm left third for text,
calm bottom-right ~1.5in for QR, right half supporting the screenshot, NO
text/letters/logos), generates the bg via the configured image provider
(postcardAiImageProvider), then a **vision pass reads the palette BACK from the
pixels** (postcardThemeReviewBg→ accent/ink/dark, readsraw_text). It then
composes a REAL sample card (live screenshot + demo code/QR) and runs a second
vision gate (postcardThemeGateCard) for legibility/layout/design; score
>=postcard_theme.gate_pass_score(default 78) savesstatus=active, else
draft. Themes are data inpostcard_themes(theme_key, source
seed/ai/operator, style_prompt, bg_type color|image, bg_value, accent/ink/dark,
status, gate_score/reasons, preview_url); postcardFactoryThemes() merges seeds
(paper/navy/kraft) + active DB rows so a passing theme is instantly pickable in
the Factory. Image backgrounds get an auto legibility scrim behind the left text
column (postcardFactoryHtml$bgIsImage).postcardFactoryHtml(...,$themeSpec)
takes a candidate spec so the Studio can preview before saving.
Dev API: dz factorytheme do=create style="..." [b=<id>] [match=1],
do=status key= set=active|rejected,do=list. Admin: AI Postcards page has the
AI Theme Studio card (style + business picker + match-site) and a theme gallery
(activate/retire). The AI Postcards business picker uses the same
admin_business_search autocomplete and its dropdown is opaque. First run:
"beach vacation" scored 92, shipped a genuinely good card ("Tropical Escape").
dz factory compose b= [theme=] [angle=]. The old full-AI painter (below) is the
fallback (engine='ai'), kept for background-art experiments.
Themes (v2.2.382): paper (light editorial), navy (bold dark), kraft (warm
local). Theme colors derive from a dark flag + optional panel/muted/chrome
overrides — adding a theme is a few lines in postcardFactoryThemes(). Layout:
local eyebrow + headline that auto-sizes to its length (any Machi angle fits
the fixed 6x4 card) + screenshot as a real<img>in aoverflow:hiddencrop
box (full nav, no white gap) inside browser chrome showing the real domain.
Render notes: QR inlined as a data URI (external QR service timed out the
browser); networkidle2 + paint delay; 429 backoff-retry.
Load-bearing bugfix shipped here: claimCodeCreate() validated the DEFAULTED
intent but assigned the RAW $opts['intent'], so any caller not passing intent
(every postcard) failed with "Column 'intent' cannot be null" and got no code.
The AI cards hid it by inventing a code; the Factory surfaced it. Real codes now
mint network-wide.
Every send already mints its OWN claim code + doozer.link shortlink + Thanks.io order_id, so each touch is independently trackable — the schema never limited a business to one card. The admin Sent section (v2.2.409) is bounded so it never runs wild at scale: three last-5 glances (Sent / Delivered / Touched = scan·visit·claim) plus a full All sends table with search (business/code), campaign + status (delivered/scanned/claimed) filters, sort-by-date, pagination, and a Doozer+ column = full conversion (claimed + owner on a premium tier).
postcardDesignLiveStats(array $design) (postcard-ai.php) reads outcomes LIVEfrom thanksio_sends (cost/order/preview), thanksio_events (delivered + QR scans), shortlinks.click_count (visits), claim_codes.redeemed_at (claimed). Never the cached JSON. postcardAiTracking() delegates to it + caches. The old code queried a non-existent shortlinks.visits column (always 0) — fixed to click_count.
thanksIoHandleWebhook) recomputes the linked design's tracking on everydelivered/scan event, so the view stays fresh without a manual Refresh.
?ajax=resend on admin-postcards-ai.php re-runs postcardFactoryCompose()for the same business + campaign + theme (theme recovered from model = factory:<key>) → a NEW pending_review card with a fresh code. Never auto-mails; operator Previews / Sends Real. Delivered/scan status fills in as Thanks.io reports it (minutes→days).
Factory front + the real Thanks.io handwriting back. Proofs persist to postcard_designs.proof_urls on preview/send; ?ajax=proof fetches them free on demand for older sends (a preview of a sent card is allowed — free, no mail) and caches. NOTE: Thanks.io previews live at response.data.previews (front + handwriting back, S3 URLs).
authorization_total/leads_fee ALREADYin cents (114 = $1.14 for a 4x6, 1 recipient); the send path was multiplying by 100 again. Dropped the *100 (thanksio.php ~357); dz postcard do=fixcosts re-derives historical rows from raw meta.
businesses.logo_url, anunreliable scrape (often a site PHOTO, not a logo). Slot removed ($logoBlock=''); the screenshot already shows real branding. Re-enable only behind a trustworthy is-a-logo signal.
re-render — pick per send via the review-card selector (4x6/6x9/6x11). Plumbs postcardAiSend → thanksIoMailClaim → Thanks.io; recorded on thanksio_sends.size.
postcardFormatPerformance($niche,$state,$days) + a "Formatperformance" admin panel — funnel (sent→delivered→scanned→claimed→Doozer+ converted) by SIZE as a rate of sent, sliceable by niche/state/timeframe, with cost/claim + a thin-sample flag (n<30). Claim rate is the money metric.
prospects; every touch (postcard sent/delivered/scanned/claimed, site created, leads we deliver to them) rolls into a per-business awareness score → internal Hot Leads to convert free→Doozer+→Keeper→agency. Reconcile with [[machi]] rather than duplicate.
direct-mail + AI marketing to their OWN leads. Needs a getApiKeyForBusiness() seam off the single getApiKey() chokepoint, per-business thanks.io key, owner send tool on manage-business.php, a 50% affiliate revenue ledger, agency/franchise tiers. Reuses the Factory + the two-score /frontend-design vision gate already built.
What: The physical arm of the observed-world flywheel. When a business has no email on file (unclaimable via implicit verification), we mail a real postcard carrying the site's own screenshot, a claim code, and a scannable QR. The card DESIGN is AI-generated end to end and passes a vision-model quality/accuracy gate before any human sees it. No templated fallback — if the AI can't produce a good card in 3 tries, it fails loudly for review instead of mailing slop.
Engine — includes/postcard-ai.php
postcardAiGather($businessId) — assembles real ingredients: live sitescreenshot (siteScreenshotUrlForHost, Cloudflare Browser Rendering -> Spaces), a minted claim code + doozer.link QR (claimCodeCreate), business facts, logo.
postcardAiBuildPrompt($ctx) — detailed 6x4 postcard art-direction prompt:use the provided screenshot faithfully as the browser-framed hero, exact business name / claim code / URL, reserve a QR square, Doozer green.
postcardAiGenerate($prompt,$refImageUrls) — Gemini generateContent with thescreenshot + logo passed as real inlineData image inputs (Gemini is the only configured engine that accepts image inputs). Needs an image-capable Gemini key in api_keys (service gemini/google/google_gemini).
postcardAiGate($imageUrl,$ctx) — SCANS the generated image with a vision model(callVisionProvider openai/anthropic), returns JSON {suitable,score,reasons}. Pass threshold = platform_settings postcard_ai.gate_pass_score (default 75).
postcardAiRun($businessId,$opts) — orchestrates gather -> up to 3generate/save/gate attempts. Suitable design -> pending_review; three failures -> failed with all three attempts + reasons kept.
postcardAiSend($designId,$opts) — mails via thanksIoMailClaim (front_image_url= the generated card). live flag gates preview vs real mail (costs money).
postcardAiTracking($designId) — rolls up Thanks.io scan events + doozer.linkshortlink visits + claim-code redemption.
Table: postcard_designs (business_id, claim_code_id, screenshot_url, logo_url, qr_url, redeem_url, prompt, image_url, model, attempts, gate_score, gate_verdict, gate_reasons, status ENUM('generating','pending_review','failed','approved','sent', 'rejected'), failures JSON, thanksio_send_id, sent_at, tracking JSON).
Admin — includes/admin-postcards-ai.php (Admin -> Infrastructure -> AI Postcards, route postcards-ai). Human-in-the-loop: ready designs show the card + gate score with a Send button (live checkbox); sent designs show live tracking; failed designs show 3 attempts + Retry. Generation runs as a background postcard_ai job (dispatcher case), so the screen never blocks.
Dev API: dz postcard gen b=<id> (queue+run), list, show id=, send id= [live=1], retry id=, track id=.
The claim loop: sending sets sent and starts the state machine; a QR scan or code redemption sets the owner's businesses.admin_email — the postcard IS proof of claim (implicit verification, no claims table). See [[claim-codes]], [[thanksio]], [[the-observed-world]].
State: shipped v2.2.379 (Jul 7 2026). Blocker for first real run: an image-capable Gemini key must be in api_keys; vision gate keys (openai/anthropic) already present. First target = Powell Flooring (business id 1, West Milford NJ, email NULL, screenshot already captured to Spaces).