Verbomatique Secondaire — Project Knowledge

Last updated: August 24, 2026

Provenance of this rebuild (24 Aug 2026). The 20 Aug file was lost. This file was rebuilt from the 19 Aug copy, with the 20 Aug edits recovered verbatim from the conversation that produced them, plus the 24 Aug additions, the Reddit platform guidance, and four corrections. One passage could not be re-sourced and was deliberately left out — see “Known gap in this file” at the foot of the Advertising section.

What belongs here, and what doesn’t. This file holds knowledge that lives outside the repo: store declarations, ad account IDs, editor quirks, campaign decisions, legal constraints, and why choices were made. Facts inside the repo — code invariants, architecture, testing conventions — belong in the repo’s CLAUDE.md, which Claude Code reads automatically every session and which is versioned alongside the code it describes.

Rule of thumb: if a fact would be invalidated by a refactor, it belongs in CLAUDE.md, not here. Never record file line numbers in this file; name symbols and give a command to re-derive the answer.

The Product

What it is: A Flutter mobile app that helps secondary school students practice French verb conjugations. Three practice modes: cover-and-recall, flashcards, and type-it-out. After practice, students test themselves and get a score report. Purely on-device, no server component.

Who it’s for: Teenagers in secondary school studying French. Two live markets:

  • nl-BE (Dutch-speaking Belgium) — the original audience
  • en-CA (English Canada) — French immersion students, excluding Quebec

The buyer is usually not the user. Students are typically minors; the decision-maker is usually the parent (30-55, stressed about homework) or teacher. Ad targeting, copy, and site messaging address parents, not students. Meta and Google both restrict targeting minors directly.

But the audience is not uniformly minors. Adult FSL/immersion learners in Canada (francisation, newcomers, federal bilingualism candidates), teachers evaluating on their own device, parents who installed it on their own phone, and adult self-learners are all real cohorts. For them no minor-related restriction applies. Any design that assumes 100% minors will be wrong; the correct shape is a fork.

Code & deployment: Flutter, stored on GitHub, exhaustive unit and integration tests. Deployed on both App Store and Play Store. App ID: be.teamwabisabi.verbomatique_secondaire. Apple App ID: 1611881585.

Developer: Dries Bellen (decibellen@gmail.com)


The basic set is 32 verbs, and there is currently no gated verb content

Verified 25 Aug 2026 by Dries, directly in the app: 32 verbs, all in the basic set, no IAP required for any of them. Independently derived the same day from the source, by tracing what the picker actually reads rather than by counting files:

  • FrenchRepository._internal() registers 32 verb instances, keyed by infinitive.
  • 32 distinct infinitives, no duplicates — the map collapses nothing.
  • The picker is verbs.keys.toList()..sort() with no where, no purchase or entitlement filter. Nothing is hidden from the wheel.
  • The only IAP product in the codebase is remove_ads_lifetime. There is no verb-pack purchase at all, so no verb can be locked.

That chain — registration list → VerbRepository.verbs.keys → the wheel — is the re-derivable source. scripts/list_verbs.py on harness/screenshot-pipeline parses it, so the number can be regenerated rather than remembered.

This is stable knowledge: verb content changes only when someone deliberately adds verbs, not as a side effect of any other work. It is recorded here rather than in CLAUDE.md because it is a product scope fact that marketing, store copy and IAP planning all depend on, not a code invariant.

Because extra verb-set IAPs remain Planned, NOT live, the basic set is currently the whole set — confirmed by the absence of any verb-pack product, not merely by the roadmap’s status label. Any of the 32 is fair game for a screenshot, a card or a store claim.

Nothing publicly states this number. Checked 25 Aug 2026: the App Store listing, the Play Store listing, verbomatique.com and secondaire.verbomatique.be all state no verb count; the .be site refers to “extra werkwoordensets” only as upcoming. A concrete “32 verbs” is the kind of specific the listings currently lack and search intent rewards — worth a decision rather than an omission by default.

Source: Dries, direct check in the app, 25 Aug 2026; file count from a Claude Code session the same day

Monetization (current state as of Aug 2026)

Tier Status Price What you get
Free Shipped $0 Ad-supported. Basic verb set.
Paid (one-time) Shipped €1.99 (BE) / CAD $1.99 (CA) No ads. Same basic verb set. Store floor price in each region.
Subscription Planned TBD No ads + the entire verb catalogue, including sets added later. Pricing not decided.
Extra verb-set IAPs Planned, NOT live TBD Verbs beyond the basic set, sold as sets — one SKU per set. Not built.

Important: When extra verb-set IAPs ship, add IAP mention back to both site pricing sections. Currently deliberately omitted since they’re not purchasable.

In-app upgrade nudge (Free → Paid): Design work done 18 Aug 2026, tracked in Asana (“In-app upgrade nudge (Free → Paid)”). Three decisions are open as subtasks there. Do not re-litigate the design in this file; the Asana task holds the current state.

Design summary as decided: three lanes keyed on a platform age signal. Lane A (18+) gets the full nudge after the score report on a completed session — explicitly not ad-adjacent. Lane B (under 18) gets no in-app exhortation at all; Paid stays passively discoverable in Settings. Lane C (signal unavailable or declined) is treated as Lane B and is the safe default. For lanes B and C the offer relocates to adult-to-adult channels already owned: the Brevo Ouder/Parent segments and both sites’ pricing sections.

UCPD Annex I point 28 (Belgium: Boek VI WER, art. VI.100, 28°) blacklists, as unfair in all circumstances with no balancing test, “including in an advertisement a direct exhortation to children to buy advertised products or persuade their parents or other adults to buy advertised products for them.” The EU CPC common position applies this to in-app purchase prompts and extends it to apps merely likely to appeal to children, not only apps targeted at them.

Practical rule: an “ask a parent to unlock” / share-link / QR handoff shown to a minor is more exposed than a plain Buy button, not less. Neutral, non-imperative disclosure that a paid tier exists (the current Settings placement) sits outside the prohibition. Not legal advice — worth a Belgian consumer-law solicitor’s review before building any upsell.

Platform age signals (relevant to the lane design)

  • Apple Declared Age Range API — global since February 2026. Available in BE and CA today.
  • Google Play Age Signals API — beta. Brazil since March 2026, Canada from mid-August 2026, full global rollout stated for later in 2026.

Both return a band (0–12 / 13–15 / 16–17 / 18+ by default), plus whether the signal was parent-set, self-declared or verified, and both can return declined or verification required.

Market asymmetry: en-CA has Apple available and Play rolling out now. nl-BE has Apple available, Play not until end of 2026. So nl-BE Android is effectively unaddressable for an age-gated in-app nudge this autumn.

Affordability is the pricing principle, not just the Paid price point

Affordability is a stated product value and the reason behind pricing decisions, not merely a description of their outcome. It is why Paid sits at the lowest price each store allows in each region — a volume play, not a margin play — and it is what selects sets over individual verbs for paid verb content.

Recorded here because the value was previously visible only through its instances: the €1.99 / CAD $1.99 figure and the “store floor price in each region” note. An instance does not survive a change of circumstances the way a principle does, so a future pricing decision had nothing to re-derive the reasoning from.

Source: Dries, in conversation, 25 Aug 2026

Verbs beyond the basic set are intended to be sold as sets — one store SKU per set, unlocking several verbs — not verb by verb. Bundling is the whole point: it is what holds the per-verb cost down, which is the affordability principle applied to this decision.

If sets cannot be sold as a unit, the stated fallback is to sell verbs individually at the lowest price each store allows.

The fallback is a degradation, not an equivalent option, and should not be defaulted into. Unbundling keeps each purchase cheap while multiplying the cost of the catalogue: content that would have been one set SKU becomes N SKUs, none of which can price below its region’s store minimum. If the fallback is ever triggered, the responses that actually serve affordability are a smaller paid surface — more verbs moved into the basic set — or no paid verb content at all. The same catalogue at per-verb pricing is not one of them.

Status is unchanged by any of the above: not built, not live. The only IAP product in the codebase is remove_ads_lifetime (verified 25 Aug 2026, see “The basic set is 32 verbs” under The Product).

Source: Dries, in conversation, 25 Aug 2026

A Subscription buys the catalogue, not a snapshot of it

The Subscription tier includes the entire verb catalogue, including sets added after a subscriber joins — not “whatever IAPs happen to exist at the time of purchase”. The catalogue is maintained and extended at Verbomatique’s discretion; the commitment is to access, not to any rate of new content.

This has an implementation consequence. An entitlement flag that grants everything present and future satisfies the commitment; bundling the then-current set SKUs silently breaks it the first time a set is added, and breaks it for existing subscribers rather than visibly at purchase. The roadmap lists “unlock flag vs. bundling” as an open question — it is already decided in one direction by what the tier promises.

Source: Dries, in conversation, 25 Aug 2026


Store Declarations (verified 18 Aug 2026)

App Store Connect

  • Category: Education. NOT in Apple’s Kids Category, so none of its restrictions on external links, purchase flows or third-party SDKs apply.
  • Age rating: 4+ in 172 countries/regions; 12+ Brazil; All ages Korea; 00+ All ages Vietnam.

Google Play Console

  • Target age groups declared: 13–15, 16–17, 18 and above. No band below 13.
  • Therefore Google Play Families Policy does NOT apply — no certified-ad-SDK requirement, no mandatory neutral age screen, no Families review burden.
  • Ads: “contains ads” declared, “Bevat advertenties” label shown on the listing.

Asymmetry worth remembering: Apple 4+ vs Play 13+. These answer different questions (Apple = content suitability, Play = target audience), so it is not an inconsistency to fix. But it means the App Store surfaces this app to every age band, so the real installed base likely skews younger than the Play declaration implies. Treat “unknown age” as the common case, not the edge case.


Primaire’s Play target-age declaration is wrong, and correcting it now would create a violation

Play Console → App-content → Doelgroep en content, last edited 17 jul 2021. Ticked: 13-15, 16-17, 18+. Unticked: five-and-under, 6-8, 9-12. That is identical to what Secondaire declares — almost certainly never updated when the two products split. The store listing itself says “leerlingen uit het lager onderwijs”.

This is why nothing has ever flagged: with no under-13 band declared, Google Play Families Policy has never applied to Primaire — no certified-ad-SDK requirement, no advertising-ID restriction, no enforced privacy-policy requirement.

Do not tick 9-12 yet. The moment an under-13 band is declared, Families Policy applies — and the shipped Android binary still contains Firebase Analytics. One checkbox would turn an app that is currently not in breach into one that is. Correct sequence: ship the Firebase removal first, then correct the age band, then work through Families Policy (content rating may need redoing, listing may need re-review, and the Advertentie-ID declaration of 23 aug 2023 needs revisiting).

Source: Asana 1217744288172445, comment of 22 Aug 2026 16:04

Apple binds the privacy policy URL to an app version; Play does not

App Store Connect, app 1587030454: the Privacy Policy URL field is read-only. Apple states, on both App Information and App Privacy: “To make changes to the app name, category, or privacy policy, create a new app version. All other changes will be immediately available.”

So a dead privacy-policy URL cannot be fixed on Apple without submitting a build. Play allows it as a standalone metadata change.

Source: Asana 1217744288172445, comment of 22 Aug 2026 16:21

Both Primaire store listings declared a 404 privacy policy — Play fixed 22 Aug, Apple still broken

Both listings declared https://verbomatique.be/privacy-policy/, which returns “Pagina niet gevonden — 404”. On ASC it appears twice: inside the App Privacy panel and under Information.

  • Play — fixed. New URL submitted and under review; Publicatieoverzicht confirms it. Managed publishing is OFF so it goes live automatically once review passes. Note: after the update the Privacybeleid row disappeared from the “Actie ondernomen” tab, which usually means pending publication rather than live — worth re-checking the public listing.
  • Apple — still broken and will remain so until a new version ships (see above).

Source: Asana 1217744288172445, comments of 22 Aug 2026 13:02, 15:49 and 16:21

The Data Safety asymmetry: Android’s declaration is false, Apple’s is true

Play Data Safety for Primaire states “Geen gegevens gedeeld met derden” and “Geen gegevens verzameld”, while the shipped Android binary contains Firebase Analytics (confirmed in the merged release manifest from main: FirebaseInitProvider, AppMeasurement services, INTERNET, ACCESS_NETWORK_STATE, install-referrer permission). Play’s scanner detects SDKs by presence in the binary, not by whether they initialise.

Apple’s “Data Not Collected” is accurate — the SDK audit found no Firebase on iOS at all (Podfile.lock has only Flutter, integration_test and shared_preferences_foundation; no GoogleService-Info.plist). The exposure on the Apple side is a broken link, not a false statement. The Firebase problem is Android-only; do not over-scope the fix.

The Firebase removal is what makes the existing Play declaration true — it is not a tidy-up. Currently the declaration describes the app that was intended; the removal makes the binary match it.

Source: Asana 1217744288172445, comments of 22 Aug 2026 13:02 and 13:09

Store account identity and IDs (verified 22 Aug 2026)

  • Google Play — developer shown as “Team Wabi Sabi” (missing the B.V.). Account flagged PERSOONLIJK ACCOUNT, Account-ID 5658320062707344136. If a personal account constrains the developer name, converting account type is substantial work with verification requirements, not a field edit.
  • App Store Connect — provider is the individual “Dries Bellen”; the address already reads Gingelom, so only the entity is wrong. Changing an Apple account’s legal entity is a support case and may require a D-U-N-S number.
  • Apple App IDs: Primaire 1587030454, Secondaire 1611881585.
  • Primaire’s live iOS build is version 3.0.1, released 24 April 2022 (27,1 MB, age rating 4+, Education, free). Play’s listing was last updated 28 Aug 2025 (100+ downloads, PEGI 3). Version 3.0.0 was literally “Verbomatique heet nu Verbomatique Primaire”.
  • applicationId mismatch: be.teamwabisabi.verbomatique on Android vs com.teamwabisabi.verbomatique on iOS. Pre-existing, permanent, worth knowing before store work.

Before anything else on the entity question: check whether Team Wabi Sabi B.V. already has a D-U-N-S number. It is free, takes up to about two weeks, and is a prerequisite on both platforms — that single step may determine the whole timeline.

Sources: Asana 1217744288172445, comments of 22 Aug 2026 13:09 and 16:12; Asana 1217746710233128 (created 22 Aug 2026 16:15)

Status: built, committed, not yet released. Branch consent/ump-flow, five commits ahead of main (verified 25 Aug 2026). Not merged, not shipped. Everything in this section — ConsentManager, ConsentApi, the two-flag split and canServeAds — exists only on this branch; on main and on the harness branches there is no consent flag at all and the single knob is AdsController.isAdvertisingEnabled via --dart-define=DISABLE_ADS=true. Cite this section’s invariants only for work on this branch.

Architecture as built

  • ConsentManager (lib/consent/consent_manager.dart) — ChangeNotifier, provider-injected via ChangeNotifierProvider.value, created in main() before runApp. Deliberately not a singleton; matches how AdsController, PurchaseService and SettingsController are wired.
  • ConsentApi (lib/consent/consent_api.dart) — thin seam over the UMP SDK’s static entry points so tests can inject a fake. DefaultConsentApi holds the real ConsentInformation and ConsentForm calls.
  • Two-phase startup: kickoffConsentInfoUpdate fires from main() without await, so runApp is never blocked. resolveConsentWithForm runs from SplashScreenWidget via addPostFrameCallback and joins the in-flight call through a Completer.

The two-flag split — do not collapse these

  • isAdvertisingEnabled = entitlement. Written only at AdsController construction and in the purchase listener, both in main(). Nothing else writes it.
  • _consentAllowsAds = consent gate, written only via updateConsentGate from the ConsentManager listener.
  • canServeAds = the AND of both. Every ad-serving decision reads canServeAds; zero ad-serving reads of isAdvertisingEnabled remain.
  • Why it matters: merging these into one boolean would let a purchase-status re-evaluation silently resume ads for a user who withdrew consent.
  • Verify with grep -rn "isAdvertisingEnabled" lib/. The authoritative version of this invariant lives in the repo’s CLAUDE.md — if the two ever disagree, CLAUDE.md wins, because it is versioned alongside the code.

Timeouts

  • Info update: 5 seconds. On timeout or failure, falls through to the SDK’s cached canRequestAds value. Deliberate — a returning user who consented last session keeps ads on a slow connection.
  • Form load: 10 seconds on the load step only. Display is unbounded, because the user has to read and respond. Two-step loadConsentForm + form.show, not the combined API.
  • Known trade-off: a first-launch user on a captive portal gets no ads that session regardless of jurisdiction, including in Canada where consent was never required. Fail-closed is correct here.

ATT

iOS only. Fires inside resolveConsentWithForm after the UMP form is dismissed and before notifyListeners. Called from addPostFrameCallback so the window is guaranteed visible, which the system prompt requires.

Revocation and re-grant

Revoking from Settings tears down rendered banners (Consumer-based teardown in the route widgets), not just a flag flip. Re-granting re-initialises without an app restart. Both paths covered by widget tests.

advertising_id

Removed entirely. Both AdvertisingId.id(true) call sites discarded their return value and nothing in the codebase consumed the advertising ID, so it was dead code as well as pre-consent GDPR exposure. Package dropped from pubspec.

Tests

178 before this work, 208 after. New files:

  • test/consent/consent_manager_test.dart — four consent states, both timeout paths, revoke/re-grant, privacy-options status
  • test/consent/ads_controller_test.dart — flag isolation, listener semantics
  • test/consent/banner_revocation_test.dart — rendered banner disappears on revocation

flutter analyze went from 10 issues to 9; all remaining are pre-existing in untouched files.

ATT sits outside the ConsentApi test seam (24 Aug 2026)

ConsentManager._requestATT() calls the plugin directly, bypassing the ConsentApi seam. ATT is therefore unreachable by all 208 tests — injecting the existing fake does not suppress the native prompt. Demonstrated 24 Aug when a screenshot run popped the real iOS dialog despite the fake being in place.

Compounds with the known missing try-catch: the one path that can silently kill iOS ad serving for a whole session is also the one path outside the test seam.

Also: earlier marketing screenshots were clean only because that simulator’s ATT status happened to be already answered. It has since reset. That is direct evidence for why C6 device QA is the release gate.

Outstanding

  • _requestATT has no try-catch. If requestTrackingAuthorization throws, notifyListeners never fires and no ads serve for the session on iOS. Two-line fix, not yet applied.
  • tagForUnderAgeOfConsent is still false at the call site. Blanket true for nl-BE was decided (see C2); the one-line change is task C5.
  • No device testing yet. All 208 tests run against fakes; nothing has exercised a real UMP form or a real ATT prompt. That is C6 and it is the release gate.

Belgium took the GDPR Art. 8(1) option to lower it from 16; Belgian DPA decision 31/2020 treats 13 as the threshold. The declared 13–17 audience self-consents, so no parental-consent flow is needed for them. An earlier repo audit assumed 16 and over-scoped the work by days — keep this correction visible.

Residual: the App Store rating is 4+, so under-13s do install, and for those users Belgian parental consent genuinely does apply. That cohort is not currently identifiable.


The Primaire Android build carried a Firebase/AdMob fossil — removed 22 Aug, not yet shipped

The Dart layer and the iOS build are clean. The native Android build was not, and none of it appeared in pubspec:

  • firebase-bom:28.2.1 and firebase-analytics declared in android/app/build.gradle
  • google-services.json present — Firebase project verbomatique-7630e, project number 617354334057, containing an API key and OAuth client ID
  • an orphaned AdMob application ID in the manifest: com.google.android.gms.ads.APPLICATION_ID = ca-app-pub-9014261072038331~2554378822, with no Mobile Ads SDK behind it — almost certainly a fossil of the original monetisation attempt, the one abandoned precisely because advertising to under-13s is not permissible

Removed on branch remove-firebase-admob (committed 22 Aug, not pushed, not merged, not released). Merged release manifest before: INTERNET, ACCESS_NETWORK_STATE, WAKE_LOCK, BIND_GET_INSTALL_REFERRER_SERVICE, FirebaseInitProvider, AppMeasurement services, ComponentDiscoveryService, AdMob APPLICATION_ID. After: only the Flutter self-permission DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION.

The rule this produced: built and committed is not shipped. Do not publish a policy claiming “fully on-device, no third-party data collection” until the removal is merged and released to both stores. The published Primaire policy handled this correctly — it states the shipped Android build includes the Firebase Analytics library, that the app does not use it, that the config is not activated so it probably does not initialise, that we cannot claim it never collects, and that it is being removed.

A claim that turned out to be false: the earlier audit said an AD_ID permission was merging into a children’s app. It was not — firebase-bom 28.2.1 predates play-services-measurement declaring that permission. Do not repeat the AD_ID claim. What the removal actually strips is INTERNET, ACCESS_NETWORK_STATE, WAKE_LOCK, the install-referrer permission, the Firebase init provider and the measurement services. Play’s SDK scanner would still have detected Firebase Analytics in the binary.

Source: Asana 1217744288172445, comments of 22 Aug 2026 09:59 and 10:41

Websites

nl-BE: secondaire.verbomatique.be

  • Hosting: Combell (self-hosted WordPress)
  • Theme/builder: Kolibrie/Colibri (Customizer-based). Free tier — Shortcode and Subscribe Form components are PRO-locked.
  • Editor quirks:
    • “Bewerken in standaardeditor” hangs/freezes — do NOT use. Always use “Bewerken in Colibri” (opens Customizer).
    • Inline text editing: single-click to select block → double-click for word-select → Cmd+A to select all → type replacement. Works for pre-existing elements.
    • Freshly-added “Component” elements (dragged in) are NOT editable in the same session. Pre-built “Blocks” (clicked from the Blocks tab) ARE immediately editable. Use Blocks, not Components, when adding new content.
    • The Customizer’s preview iframe is cross-origin — JS can’t access it directly.
    • Always publish immediately after editing. Navigating away triggers a native “discard changes?” dialog that can wipe unsaved work. Colibri’s autosave sometimes recovers drafts.
  • Structure: Header (Navigation + Hero [global]) / Content (page-specific blocks) / Footer (Copyright). Front Page has its own dedicated template that overrides the global Header hero.
  • Site-wide hero: “Franse vervoegingen. Zonder ruzie.” (updated Aug 2026, was “Herexamen Frans?”)
  • Pages: Front Page, Cookiebeleid (EU), Herexamen landing page, Hoe werkt, Hoeveel kost (pricing FAQ), Over, Wat kan er beter, Wat mag je verwachten
  • Signup form: Brevo shortcode [sibwp_form id=2] embedded on homepage via Colibri block, with envelope icon and “BLIJF OP DE HOOGTE” heading. Ouder/Leerkracht radio buttons.
  • Plugins: Brevo, Forminator, Defender, Complianz, Colibri, Contact Form 7, consentmanager

en-CA: verbomatique.com

  • Hosting: WordPress.com Business plan
  • Editor: WordPress Site Editor (Block editor / Gutenberg). Code editor via Options menu → Code editor, or Cmd+Shift+Alt+M.
  • Template: Blog Home (single-page site, no separate pages except Cookie Policy)
  • Current revision: 35 (as of 19 Aug 2026)
  • Signup form: Brevo shortcode [sibwp_form id=2], different form from nl-BE. Fields: First name, Last name, You are (Parent/Teacher/Other), Email. Linked to “Canada (English) Subscribers” list.
  • Plugins: Site Kit (GA4 + Search Console), Brevo, MailPoet (unconfigured), Complianz, Jetpack, WPCode, Code Snippets
  • Site Kit Analytics config: Property 548294070, Measurement ID G-7H79V4X8Y7, Google Tag GT-MBNVS8CR. “Exclude Analytics: All logged-in users” is ON (by design — means you can’t test tracking while logged in).

WordPress.com MCP connector: usable for content edits since 24 Aug 2026. Scopes were raised to Read 25 of 38 and Write 62 of 90, with Content Authoring, Site Editing and Site Editor Context enabled for write. See Website Editing for the scoping model and how to reverse it.

Previously, until 24 Aug 2026: every content operation was disabled in MCP settings (pages., posts., page-sections., patterns., media., comments., tags., categories., content.search), and only site-editor context reads worked. That is why older notes in this file say all content edits must go through Claude in Chrome. Do not act on that instruction.

All five vb-screenshot-* assets are live on the homepage (verified 24 Aug 2026 by fetching the public page):

File Where it renders
vb-screenshot-practice.png hero, under the store badges
vb-screenshot-verb-select.png step 01, “Set up the session”
vb-screenshot-practice-1.png step 02, “Practice with hints”
vb-screenshot-evaluate.png step 03, “Test without them”
vb-screenshot-results.png step 04, “See the score”

Do not delete or replace any of them. Steps 02–04 are the three blocks already in an invalid state in the editor. practice and practice-1 are not duplicates — they are different screens in different slots. The 1732×2510 versions uploaded under -native names are bezelled captures — the output of add_bezel.py ipad_mini — added alongside rather than replacing. 1732×2510 is not a device capture resolution. The iPad mini panel captures at 1488×2266; the bezel wrap produces the larger canvas. Media Library uploads are publicly reachable by URL.

Where the confusion comes from, and why the filename cannot be trusted: the harness README uses the word “native” for both things — “Native frame 1488×2266” in one place, “the full native bezelled canvas, 1732×2510” in another. In the harness, -native actually marks BEZEL_WIDTH=0, which skips the downscale step, so it means un-resized, not un-bezelled. 1732 is also the ceiling: requesting more upscales by interpolation. Read -native as “full-size bezelled”, never as a capture spec. Verified 26 Aug 2026 against scripts/README-screenshots.md on harness/screenshot-pipeline. See “There are two screenshot harnesses, on different platforms, for different locales” under Technical Patterns.

New page: /french-homework (post 173). Published 24 Aug 2026, verified anonymously — HTTP 200, form present, store badges present, indexable. Renders with header, logo, tagline and footer; the page title renders as the H1. It is the destination URL all three Blaze campaigns point at.

Superseded 24 Aug 2026. This previously read “Draft as of 24 Aug … Outstanding: store badge images (currently text buttons), one screenshot block, and a live form submission test.” The page was published later the same day and the text buttons were replaced with real badges. Whether the remaining screenshot block was added is not verified. (Asana 1217802736470180, 24 Aug 15:37; Asana 1217801744397059, 24 Aug 19:41.)

Footer is theme placeholder — Product / Company / Resources columns each contain only “Cookie Policy (EU)”. Live on the homepage too.

Three invalid blocks in the “Practice, then prove it” section. Steps 02/03/04 (the tablet-mockup image blocks) show “Block contains unexpected or invalid content / Attempt recovery” in the editor. Confirmed pre-existing as of 18 Aug 2026. The public front end renders them correctly, so this is editor-side only. Risk: editing that section visually may let Gutenberg “recover” them and rewrite their markup. Use the code editor if it must change.


verbomatique.com and verbomatique.be are architecturally different, and that decides how each is edited

The 23 Aug conclusion that “the WordPress REST API does not work” was correct for verbomatique.be and wrongly generalised to verbomatique.com.

  • verbomatique.com runs the block theme iotix, with its homepage in a block template (iotix//home) that is fully exposed over REST. The homepage is not under Pages — show_on_front is “posts” and page_on_front is 0, so looking under Pages finds nothing and looks like missing content.
  • verbomatique.be runs Colibri, with content in theme mods, which REST does not expose. Its pages really are 144–172 character stubs.

Three edit routes, one per situation: MCP connector (preferred, .com), REST via browser JS (fallback, .com), Customizer (only option, .be).

Known fingerprint for iotix//home: 53 opening block delimiters, 52 closing, 4 store-badge links. The 53/52 mismatch is pre-existing — the three invalid blocks in “Practice, then prove it” — not damage, and not to be fixed in passing.

Source: Asana 1217790212813686 (created 24 Aug 2026 09:03), recording the rewrite of project-knowledge-website-editing.md

The live vb-screenshot-verb-select.png does not show the verb it claims to

Verified 25 Aug 2026 by reading the app source. VerbSelectionmodeSelectionRoute’s selectForTesting helper updated the selection indices and the highlight colour but never scrolled the four ListWheelScrollView controllers. The chosen item therefore stayed off-viewport and was never built, so every screenshot taken through that helper captured default wheel positions, not the selection.

This asset renders on the verbomatique.com homepage under step 01, “Set up the session”. The step caption describes choosing a verb; the image does not show one chosen.

Fixed in the app on branch harness/screenshot-pipeline (the helper now jumps each wheel, tense last, because the mode wheel’s onChanged resets the tense wheel). The live asset still needs regenerating and swapping — and the standing instruction not to delete or replace the five vb-screenshot-* assets applies, so treat that as a deliberate swap rather than a routine upload.

Source: Claude Code session of 25 Aug 2026 reading lib/routes/verb_selectionmode_selection_route.dart; asset inventory verified 24 Aug 2026

Website Editing

Consolidated 24 Aug 2026. Replaces all prior scattered notes on editing the two sites.

The two sites are not the same problem, and the difference is the theme. verbomatique.com runs a block theme: its homepage lives in a block template that the WordPress REST API exposes in full, so atomic writes work. secondaire.verbomatique.be runs Colibri, which stores page content in theme mods / Customizer data that REST does not expose at all. A finding about one site says nothing about the other. This distinction was collapsed on 23 Aug and cost a working route.

en-CA: verbomatique.com — Route 1, REST API (preferred, atomic)

This works. A live copy edit was made this way on 24 Aug 2026 in a single atomic write. Supersedes the 23 Aug conclusion that REST was unusable for this project — that finding was specific to Colibri and was wrongly generalised.

INCOMPLETE — fill this in. The operational detail was never captured in any surviving document: the endpoint used, whether it authenticates by browser-session nonce (X-WP-Nonce) or by Application Password, the read-modify-write shape, and how the write was verified. The auth question is the one that matters: a nonce means REST is only a faster path inside a logged-in Chrome session; an Application Password means content editing on this site no longer depends on Chrome being alive at all. Cheap to re-derive — one authenticated read against the block template settles it.

The wrong generalisation is also recorded in Asana task 1217542444995927, under a heading === ROUTE RULED OUT ===, which states flatly that REST “does not work” with no site qualifier. Correcting this file does not reach it.

en-CA: verbomatique.com — Route 2, Site Editor code editor (fallback)

Editing template text via wp.data.dispatch('core/block-editor').updateBlockAttributes() does not work on this site. The block store accepts the change (a later getBlock() returns the new content) but wp.blocks.serialize() keeps emitting the ORIGINAL markup, so nothing reaches the save payload. Replacing the block wholesale with createBlock() + replaceBlocks() fails identically. The block reports isValid: true with a populated originalContent, and serialization prefers originalContent.

What works: open the code editor (Cmd+Shift+Alt+M in the Site Editor, or Options ⋮ → Code editor), write to textarea.editor-post-text-editor via Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set, dispatch new Event('input', {bubbles:true}), click “Exit code editor” to force a re-parse, verify with wp.blocks.serialize(wp.data.select('core/block-editor').getBlocks()), then Save.

Verify the saved byte length changed by exactly the expected delta — cheapest proof nothing else moved. (This cost four failed attempts on 18 Aug before the code-editor route was used.)

Still the right route for anything that must be seen rendered before saving, and for pasting new page markup — paste into the code editor, exit it, check nothing shows as invalid, then save.

en-CA: verbomatique.com — Route 3, MCP connector (available since 24 Aug 2026)

Usable since 24 Aug 2026. Read 25 of 38, Write 62 of 90 — Content Authoring, Site Editing and Site Editor Context enabled for write; Account, User Management and Plugin Management deliberately left off. Verified with wpcom-mcp-site-editing -> templates.lookup { slug: "home" } returning { id: "iotix//home", title: "Blog Home", theme: "iotix" }. Try the connector first for content edits on this site.

Previously: every content operation was disabled (pages., posts., page-sections., patterns., media., comments., tags., categories., content.search); only site-editor context reads worked. That is why this route was labelled “not available” and why older notes route everything through Claude in Chrome. The scoping model is account-wide with a per-site exclusion list — see the scoping entry at the foot of this section.

nl-BE: secondaire.verbomatique.be — the Customizer is the only way in

REST authenticates fine (nonce required; cookie alone gives 401) and returns all 8 pages, but the copy is not in them. wat-mag-je-verwachten is 172 characters; hoe-werkt-verbomatique-secondaire is 144. They are stubs. Colibri keeps homepage and page content in theme mods / Customizer data, which REST does not expose. There is no atomic-write route to this site.

WPCode is also not a route. It injects header/footer code; it cannot edit page content. (Floated on 23 Aug, corrected the same session.)

Inline editing technique. For simple pre-existing paragraph and heading blocks: single-click to select block → double-click for word-select → Cmd+A to select all → type replacement.

For a block containing multiple paragraphs or inline bold, Cmd+A is wrong — it flattens the formatting. Use: single-click to select → double-click to enter edit mode → triple-click to select just one paragraph → type.

Two traps, both hit on the first edit of 23 Aug:

  1. Triple-click swallows the paragraph’s closing line break, so the following line welds onto your sentence. Fix: click at the join and press Return.
  2. Clicking one character too far left puts the full stop at the START of the next line instead of the end of yours. Invisible at normal zoom — zoom in and check.

Budget four operations per edit, not one. Every edit needs visual verification; the editor gives no reliable feedback. Roughly an hour for fifteen edits, with a human on hand. Not safe to run unattended on a live campaign site.

Other Colibri quirks are listed under Websites → nl-BE (the standaardeditor hang, Blocks-not-Components, the cross-origin preview iframe, publish-immediately).


WordPress.com MCP scoping is an exclusion list, not an allow-list

Settings at wordpress.com/me/preferences/mcp (redirects from /me/mcp).

Before 24 Aug: Read 5 of 38, Write 1 of 90 — effectively everything disabled, which is why the 24 Aug landing-page edit had to go through a browser session instead of the connector. After: Read 25 of 38, Write 62 of 90. Enabled for write: Content Authoring, Site Editing, Site Editor Context. Enabled for read: Content Authoring, Site.

Deliberately left off, both read and write: Account, User Management, Plugin Management. None relate to content work; between them they can change the profile, invite collaborators, alter roles, and install or remove plugins on any site in the account.

The scoping model: “Site exceptions” is an exclusion list. The page states “MCP access is enabled at the account level. Disable it for specific sites here.” Access cannot be scoped to verbomatique.com only — it applies account-wide, and the only control is switching it off per site. No exceptions are currently set, so the default is access-everywhere.

To reverse: same three pages, toggle the groups back off.

Verified working after the change: wpcom-mcp-site-editing → templates.lookup { slug: "home" } returned { id: "iotix//home", title: "Blog Home", theme: "iotix" }.

Source: Asana 1217790212813686 (created 24 Aug 2026 09:03)

Branding

Design Tokens

--vb-blue:    #0072BC    (logo frame)
--vb-teal:    #0B5443    (headings, dark text, "FRENCH VERB CONJUGATION, SORTED.")
--vb-coral:   #E5402A    (accent, emotional highlight words, CTA buttons)
--vb-cream:   #FAF9F5    (background)
--vb-ink:     #16211D    (body text)
--vb-line:    #E4E1D8    (borders, subtle dividers)

Typography

Inter (or system sans-serif: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, sans-serif). No serif display font.

Blue robot-face icon. Cropped version saved as vb-logo-mark.png in a previous Claude Code session.

Key taglines

  • en-CA: “French conjugation, without the fight.” / “FRENCH VERB CONJUGATION, SORTED.”
  • nl-BE: “Franse vervoegingen. Zonder ruzie.” / “Franse werkwoorden. Knal van buiten.”

Ad creative style

Text-forward layout on cream background. No photography. Phone/tablet mockup showing real app screenshot on the right. Bold headline with coral accent on key emotional phrase. App Store + Google Play badges. Consistent across Meta, site, and print materials (judo club placemat ad).

Exception as of 24 Aug 2026: Blaze creative diverges deliberately — cream field plus a full-bleed 16–20px teal border, because cream-on-white has no edge against WordPress/Tumblr page backgrounds. Whether the border comes back to Meta creative is undecided. See Advertising → Blaze creative background.


REBRAND decided 22 Aug 2026: Secondaire → Verbomatique, Primaire → Verbomatique Junior

Verbomatique Secondaire becomes the flagship, renamed simply Verbomatique. Verbomatique Primaire becomes Verbomatique Junior.

Framing that matters for comms: from a user’s point of view the flagship rename is a shortening, not a replacement — the name they know loses a qualifier.

Why now: rebranding is cheapest when there is least brand equity to migrate — currently a few hundred installs, two small Brevo lists, ~17 en-CA leads, negligible organic search presence, almost no backlinks. Waiting for revenue means doing the expensive version later.

Timing gate: start only after both back-to-school flights end (nl-BE Meta ends 31 Aug; the en-CA work and the Blaze en-CA flight run later — the Reddit September flight was cancelled on 26 Aug 2026 and no longer gates this). Target window late September / early October, before ad spend scales. The reason is operational — store listing changes trigger review, ad creative referencing a shifting name confuses, and mid-flight measurement gets muddied.

What does not change and must not be attempted: app IDs are permanent (be.teamwabisabi.verbomatique_secondaire stays forever), Apple App ID 1611881585 stays, and the Play Store URL is built from the package ID so it does not change. Store renames keep the same ID, so ratings, reviews and install history carry over.

Biggest open decision: domain architecture. Today the root verbomatique.be serves Primaire and the flagship sits on secondaire.verbomatique.be. After the rebrand the flagship should own the root. That inversion drives redirects, Search Console, Meta domain verification and every link ever shared — decide it first.

Source: Asana 1217743999112835 (created 22 Aug 2026 08:57)

Team Wabi Sabi B.V. / Cicindriastraat 29, 3890 Gingelom, Belgium. Use this exact form on privacy policies, cookie policies, both store listings and Complianz on all three sites.

Applied and verified live 22 Aug: verbomatique.be (was “Team Wabi Sabi BV” / “3890 Jeuk”, verified corrected on /cookiebeleid-eu/), verbomatique.com (same old form, corrected), secondaire.verbomatique.be (was “TEAM WABI SABI” in block capitals with the address as just “Cicindriastraat 29”, no postcode or town — corrected and verified on /index.php/cookiebeleid-eu/).

Complianz-generated cookie policies embed the identity, so they regenerate when the setting changes.

Note the ASC listing shows Gingelom while Complianz originally said Jeuk — Jeuk is a deelgemeente of Gingelom, so both are arguably correct, but a privacy policy, a store listing and a cookie policy should not disagree on the trader’s address.

The pattern worth recording: three sites carried three different forms of the same company name and one address missing half its content, because each was configured at a different time and nothing propagates between them. The same drift produced two GA4 properties, a dead privacy-policy URL and a stale target-age declaration.

Source: Asana 1217744288172445, comments of 22 Aug 2026 15:25 and 16:12

Email voice — rules set by Dries, 26 Aug 2026

Five editorial rules were given during the welcome-email polish. They are house style for customer-facing copy in both locales, not one-off fixes:

  1. Explain the product before mentioning the price. The paragraph describing what Verbomatique is comes before the paragraph saying the app is free. Applied to all eight emails.
  2. Say what we don’t do, not what the reader doesn’t get. “Je krijgt van ons geen wekelijkse nieuwsbrief” reads stingy; “Wij sturen je geen wekelijkse nieuwsbrief” is the same fact told as a commitment. The English follows the same rule — “We won’t send you a weekly newsletter”, not “you won’t get one”.
  3. Lead an ask with the request, then the mechanism. “Let us know what got in the way. A reply to this email reaches the person who builds the app” — not the reply mechanics first.
  4. Full sentences, especially to teachers. A fragment used as a heading (“And the question.”) was rejected on the grounds that the audience is English teachers. Headings in teacher-facing copy are complete sentences.
  5. Dutch: prefer the inversion for a value statement. “Wij vinden betaalbaarheid belangrijk, daarom is de app gratis met advertenties.” And “activeer”, not “haal”, for switching on the ad-free version — you activate a purchase, you do not fetch one.

Source: Dries, in session, 26 Aug 2026

Site Copy Change Log

19 Aug 2026 — verbomatique.com pricing line. Changed “— CAD $1.99, no subscription.” to “— CAD $1.99, paid once.” Blog Home template revision 34 → 35; saved markup exactly 6 characters shorter, no other diff. Verified live on the public page.

Why: the roadmap plans a Subscription tier. “No subscription” sitting beside the price reads as a promise that none will ever exist, which becomes a misleading-pricing problem the day Subscription ships. “Paid once” describes the SKU instead of the roadmap.

Failure mode to remember: the phrase was written by Claude in an earlier session despite that same session’s own notes recording Subscription as Planned. Avoid forward-looking absolutes in pricing copy.

nl-BE needs no equivalent change — the Dutch pricing FAQ reads “Een eenmalige aankoop verwijdert de advertenties, aan €1,99” and never carried a “geen abonnement” clause.


Mailing Lists (Brevo)

Segmentation: Parent/Teacher (Ouder/Leerkracht for nl-BE) × locale. Both signup forms live on both homepages.

nl-BE Forms (secondaire.verbomatique.be Brevo plugin)

  • inline_form_parents_teachers_nlBE[sibwp_form id=2] — embedded on homepage
  • Herexamen nl_BE[sibwp_form id=3] — separate campaign form

en-CA Forms (verbomatique.com Brevo plugin)

  • Default Form[sibwp_form id=1] — basic
  • en_CA_LandingPageForm[sibwp_form id=2] — embedded on homepage, linked to “Canada (English) Subscribers” list

Consent text (both sites, updated Aug 2026): No longer mentions “paid version” or “ad-free version” as upcoming — references “new features, new verb sets, and product news” (en-CA) or “nieuwe functies, nieuwe werkwoordensets en productnieuws” (nl-BE).

Signup sends no email at all (verified 24 Aug 2026, en-CA form id=2)

  • Send a confirmation email: No
  • Double Opt-In: No
  • Redirect after subscription: Yes — to /thanks (changed 24 Aug 2026 ~15:36, after the rest of this section was verified at 14:50)

A subscriber is redirected to /thanks and receives nothing from the form itself — Brevo’s form settings still send no confirmation and no double opt-in, and that is unchanged.

But the subscriber is no longer left with nothing, as of 26 Aug 2026. A welcome automation now fires on the contact being added to the list and sends a parent or teacher message within minutes. See “The welcome automations are live” below.

The cold-list-plus-cold-domain risk is spent. The domain was authenticated on 25 Aug and the lists were mailed on 26 Aug — 60 recipients across four backfill campaigns, zero bounces, zero unsubscribes, first opens inside five minutes. The two halves did not land simultaneously, which was the thing to avoid.

Formerly (until the 24 Aug redirect), the inline success message read “Thank you, you have successfully registered !” — the space before the exclamation mark is French typography in en-CA copy.

nl-BE equivalents not inspected.

List naming: Canada (English) Subscribers (id 3, en-CA) and België (Nederlands) Subscribers (id 2, nl-BE). The nl-BE list was called Your first list — Brevo’s default name — until it was renamed; verified renamed 26 Aug 2026.


Brevo substituted an unmonitored sender domain — FIXED 25 Aug 2026, and the reply half was never true

Resolved 25 Aug 2026 — campaigns now send as Verbomatique <hello@news.verbomatique.com>. See “The sending identity is now news.verbomatique.com” below for the records and the before/after measurements.

The reply claim in this section was wrong. Replies were never lost. The unauthenticated messages already carried Reply-To: decibellen@gmail.com — Brevo adds it automatically when it substitutes the sender — so replies from parents and teachers reached the Gmail inbox all along. Verified 25 Aug by reading the raw headers of a message sent before any change was made.

The substitution was also not uniform. A single send on 25 Aug produced two different From headers: decibellen@11688408.brevosend.com for the Gmail recipient, and the unchanged decibellen@gmail.com — which then failed DMARC — for a non-Gmail one. “Everyone sees brevosend” was never accurate.

The deliverability half of the original concern was real and is what the fix addressed: mail now passes DKIM, SPF and DMARC aligned to a domain we own, which matters most for whatever turns out to be this list’s first ever campaign.

DNS note, corrected 25 Aug 2026: verbomatique.com DNS is at WordPress.com, but the MCP connector’s DNS abilities are not enabled — the records were added through the dashboard. See “The WordPress.com MCP connector does not include DNS” under Technical Patterns. The .be domain was already fully Brevo-authenticated before any of this work; the note that it was manual and outstanding was stale.

Authentication alone does not fix the reply problem — an explicit Reply-To pointing at a monitored inbox is what does. Note the direction of risk has now inverted: Brevo stops adding Reply-To once the From is authenticated, so it must be set on every campaign by hand.

Source: Asana 1217778600752666 (created 24 Aug 2026 09:43)

Attribute completeness on the en-CA list (read 24 Aug 2026)

Brevo list #3 (Canada (English) Subscribers): 24 contacts, up from 21 on 22 Aug.

  • 24 of 24 carry LOCALE — the locale-neutral migration held.
  • 2 of 24 carry both CONSENT_MARKETING and AUDIENCE (ids 16 and 27).
  • 22 of 24 carry neither.

All four arrivals since the baseline (ids 30–33, landing 23 and 24 Aug) came in with the Meta/Pabbly shape — FIRST_AND_LASTNAME + LOCALE only. At the time, every new lead arrived unusable for Parent/Teacher segmentation and with no consent record stored in the system that actually sends the email; the consent evidence sat in Meta instead.

Superseded on the AUDIENCE half, 26 Aug 2026. The figures above are a 24 Aug snapshot and should be read as history. A complete read of list 3 on 26 Aug 2026 18:03 returned 37 of 37 contacts carrying AUDIENCE — 19 Parent, 17 Teacher, 1 Other — after the backfill reconstructed from Meta’s lead records. The list is now fully segmentable, which is what let the four welcome campaigns split cleanly by audience.

The consent half is unchanged and still a gap. Only 3 of the 37 carry CONSENT_MARKETING (ids 16, 27 and 34 — the website-form signups). For every Meta lead the consent evidence still lives in Meta and not in Brevo.

Whether the en-CA Pabbly action now maps AUDIENCE by itself is not established. The seven contacts that arrived on 26 Aug (ids 67–73) do carry AUDIENCE, but the manual backfill touched every contact on the list, so their AUDIENCE cannot be attributed to Pabbly. Testing that needs a lead arriving after this date and read before anyone touches it.

Bookkeeping correction: the 22 Aug baseline recorded ids 3, 16 and 27 as complete. Id 3 is dries.bellen@wabisabiconsulting.be and the locale migration correctly moved it to list 2 (België) with LOCALE nl-BE. So the real en-CA figure was 2 complete out of 20 genuine leads, and is now 2 out of 24. Nothing was lost — the list never had 3.

CONSENT_NL_BE no longer exists — deleted 18 Aug, confirmed absent from the Brevo schema and from all contacts on 22 Aug. The current field is CONSENT_MARKETING.

Source: Asana 1217542445377430, comments of 23 Aug 2026 08:15 and 24 Aug 2026 07:59

The Belgian list had no working lead route until 25 Aug 2026 — RESOLVED

As it stood on 24 Aug: Brevo list 2 “België (Nederlands) Subscribers” held exactly one contact — Dries Bellen, created 22 July, attributes clean (AUDIENCE=1, CONSENT_MARKETING=true, LOCALE=nl-BE). Contacts added in the week to 24 Aug: 0. Because the weekly export had never completed successfully, there was no lead route into list 2 at all.

Resolved in two moves on 25–26 Aug. The 20 leads stranded on the Meta form were imported by CSV, and the nl-BE Pabbly workflow was built and activated — since proven to be delivering. List 2 holds 23 contacts as of 26 Aug 2026. Do not carry the “exactly one contact” figure forward.

The confounding was bypassed, not resolved. Whether the website form was also regressing was never established, and there is now a working lead-ad route masking the question. If website signups matter separately from lead-ad signups, that needs its own test.

Source: Asana 1217775027491967 (created 24 Aug 2026 09:31)

The two en-CA test contacts are gone — resolved 26 Aug 2026

Two test submissions on 24 Aug went into Canada (English) Subscribers, sent to a + alias. They are no longer there. A complete read of all 63 account contacts on 26 Aug 2026 18:03 returned no +-alias address in any list, and list 3 stands at 37.

This matters for the September baseline, which is now measured against 37 real contacts rather than 37 real ones plus two fakes. The four welcome backfill campaigns sent on 26 Aug went to 60 recipients — the two lists’ exact combined membership — so no test address received one.

Source: Brevo contacts_get_contacts (full account read) and lists_get_lists, 26 Aug 2026.

The sending identity is now news.verbomatique.com (25 Aug 2026)

Campaigns send as Verbomatique <hello@news.verbomatique.com>, a Brevo-authenticated subdomain of verbomatique.com. Before this, Brevo sent as decibellen@gmail.com, which cannot be authenticated because Google owns gmail.com.

Four records live in the verbomatique.com zone at WordPress.com, all added 25 Aug 2026:

Type Name Value
TXT news brevo-code:ac1b5c6fe7ef6c9afbd0e39bb6987bec
CNAME brevo1._domainkey.news b1.news-verbomatique-com.dkim.brevo.com
CNAME brevo2._domainkey.news b2.news-verbomatique-com.dkim.brevo.com
TXT _dmarc.news v=DMARC1; p=none; rua=mailto:rua@dmarc.brevo.com

The brevo-code value is account-level, not domain-level — verbomatique.be carries the identical string. Do not treat a matching brevo-code as evidence that the right domain was verified.

Measured on an identical message body before and after, sent from a draft campaign that was never sent to a list:

  • mail-tester 3.6/10 → 7.7/10; “You’re not fully authenticated” (−3) → “You’re properly authenticated”.
  • Gmail’s Authentication-Results after: dkim=pass header.i=@news.verbomatique.com header.s=brevo2, spf=pass, dmarc=pass (p=NONE) header.from=news.verbomatique.com.

The residual −1.8 on the after-test is SpamAssassin reacting to a deliberately short test body (HTML_IMAGE_ONLY_24, HTML_SHORT_LINK_IMG_3), not to the sending configuration.

Brevo’s optional branded subdomain (own-domain tracking links in place of sendibt3.com) was deliberately not set up, to keep the change surface small. Both news.verbomatique.com and verbomatique.be currently read “Not branded” in Brevo.

Source: Asana 1217778600752666, comment of 25 Aug 2026; DNS verified against 8.8.8.8, 1.1.1.1 and 9.9.9.9; header verdicts read from the raw delivered messages.

Reply-To is per-campaign in Brevo, and nothing sets it for you any more

Brevo has no account-level or sender-level Reply-To. It is a per-campaign toggle: campaign → Additional settings → Sending and Tracking → “Use a different Reply-to address”.

While the From was unauthenticated, Brevo added Reply-To: decibellen@gmail.com automatically as part of substituting the sender. Now that the From is authenticated, it does not. Every future campaign must set Reply-To explicitly. Forget it once and replies go to hello@news.verbomatique.com, which has no mailbox and hard-bounces.

This is the single most forgettable step in the whole setup, and its failure is silent to the sender.

Source: Asana 1217778600752666, comment of 25 Aug 2026.

news.verbomatique.com accepts no mail — the From address hard-bounces

The subdomain has no MX, no A and no AAAA record. Mail addressed to hello@news.verbomatique.com fails with 5.4.3, observed directly from Google’s mailer-daemon.

Normal replies are unaffected: Gmail honours Reply-To and pre-fills decibellen@gmail.com in its reply box, verified 25 Aug by replying and confirming arrival. What does bounce is anything that answers the From instead — some auto-responders and out-of-office systems, some forwarding rules, clients that ignore Reply-To, and any recipient typing the address by hand. There is also no route for postmaster@ or abuse@ on that subdomain.

Undecided: whether to add an MX or forwarder, move the sending address to a domain that already receives mail, or accept the gap. Tracked in Asana 1217814220575319.

Source: Asana 1217814220575319 (created 25 Aug 2026), which records the bounce verbatim.

verbomatique.be was already fully Brevo-authenticated before any of this

Read from live DNS on 25 Aug 2026, before any change was made: verbomatique.be already carried the brevo-code TXT at the apex, brevo1._domainkey and brevo2._domainkey CNAMEs into dkim.brevo.com, _dmarc with rua=mailto:rua@dmarc.brevo.com, and mail.verbomatique.be as SPF plus a CNAME to mail-verbomatique-be.brand.brevosend.com. Brevo’s Domains tab showed it as Authenticated.

No .be work was needed and none was done. Nobody had recorded that it was already set up, which is why the standing note said the .be side was manual and outstanding.

However: no sender exists on verbomatique.be. An authenticated domain with no sender on it changes nothing about what recipients see — which is exactly the trap the gmail.com sender fell into. Authenticating a domain and switching the From are two separate steps.

Source: live DNS read 25 Aug 2026; Brevo Senders, Domains & Dedicated IPs → Domains.

The Brevo MCP connector is read-only for contacts

The connector exposes contacts_get_contacts, contacts_get_contact_info, contacts_get_contact_stats, contact_import_export_get_contacts_from_list, lists_get_lists, lists_get_list, segments_get_segments, attributes_get_attributes, accounts_get_account, senders_get_senders, and creation tools for campaigns, SMS campaigns and templates. There is no create-contact, no update-contact and no import.

So any contact write — a manual import, a backfill, a field correction — has to happen in the Brevo web UI or through an integration like Pabbly. This is not a scoping choice that can be raised in a settings page, as with the WordPress.com connector; the tools do not exist.

The consequence that matters: any scheduled or unattended task whose success condition is “contacts land in Brevo” cannot succeed on the connector alone. A task written as “export → import” is not slow or fragile, it is impossible.

Source: connector tool inventory, re-read 26 Aug 2026 — unchanged since 25 Aug

The AUDIENCE attribute’s enumeration

AUDIENCE is a Brevo category attribute: 1 = Parent, 2 = Teacher, 3 = Other. The nl-BE form emits ouder / leerkrachtlowercase, confirmed in the live Pabbly response — so neither the label nor the id matches without a translation step. CONSENT_MARKETING is a boolean, LOCALE and FIRST_AND_LASTNAME are text, and DOUBLE_OPT-IN is a category (1 = Yes, 2 = No).

Source: Brevo attributes_get_attributes and the live Pabbly trigger response, both re-read 26 Aug 2026

Pabbly silently drops AUDIENCE on every nl-BE lead — found 26 Aug 2026

The nl-BE Pabbly action has AUDIENCE set to None deliberately, because ouder / leerkracht do not map onto Brevo’s 1 / 2 without translation. The cost of that decision is now visible in the data rather than theoretical.

The nl-BE form does ask the question, and Meta does capture the answer: lead kirsten.fotovdw@gmail.com, submitted 25 Aug 22:30, reads Ik ben... Ouder in its Formulierantwoorden panel. The contact Pabbly created from it (Brevo id 66) carries CONSENT_MARKETING true, LOCALE nl-BE, FIRST_AND_LASTNAME Kirsten — and no AUDIENCE at all. The answer is collected and then thrown away in transit.

That is the one real gap in an otherwise working route. The 20 leads imported by CSV on 25 Aug all carry AUDIENCE (14 Parent, 6 Teacher, verified by reading them back), so the automated path is now producing worse data than the manual one it replaced. Fixing it needs a translation step in Pabbly — a router or a formatter turning ouder1 and leerkracht2 — not a change to the form.

Source: Meta Leads Centrum and Brevo contacts_get_contacts, both read 26 Aug 2026

Three contacts sit outside every list

Ids 42, 43 and 44 — decibellen@gmail.com and two srv1.mail-tester.com addresses, all created 25 Aug during the sender-authentication work — have listIds: []. They belong to no list, so they are invisible in every list view while still counting against the account. Harmless, but they will confuse the next person who compares an account total against the sum of the lists.

Account total is 63 contacts as of 26 Aug 2026 18:03 (list 3 en-CA: 37 unique; list 2 nl-BE: 23; plus the three orphans). It was 36 on 25 Aug before the nl-BE backfill, and was read as 65 earlier on 26 Aug — do not carry either older figure forward.

The drop from 65 to 63 is the two en-CA test contacts leaving the list: a complete account read of all 63 contacts contains no +-alias address at all, and the id sequence has a gap at 35 and 36, between the 24 Aug 14:21 arrival and the 25 Aug 02:14 one — the exact window the test submissions were made in. That they were deleted is the obvious reading of those two facts, but the deletion itself was not observed.

Source: Brevo contacts_get_contacts and lists_get_lists, read 26 Aug 2026

The welcome automations are live, and the whole list has now been mailed (26 Aug 2026)

Both Brevo automations are Active as of 26 Aug 2026 17:33. Verified by reading the Automations → Workflows listing directly: “Welcome · nl-BE” (#2) and “Welcome · en-CA” (#1), both showing Active, with the tab counters reading All 2 · Active 2 · Paused 0 · Inactive 0.

Each automation is triggered by a contact being added to its locale’s list and branches on AUDIENCE:

  • Branch A — condition AUDIENCE is equal to [Teacher] → the teacher message.
  • Branch BDoes not match any conditions → the parent/other message.

Branch B is a catch-all, not a Parent test. A contact with no AUDIENCE at all therefore receives the parent message rather than falling out of the automation. That is deliberate and worth knowing, because the nl-BE Pabbly route still drops AUDIENCE (see “Pabbly silently drops AUDIENCE on every nl-BE lead”) — those contacts get a welcome, just the parent one.

“Re-entry after exit” was left off on both, so a contact re-added to a list will not be mailed twice.

Source: Brevo Automations → Workflows, read 26 Aug 2026 18:05

The backfill: four one-off campaigns to the 60 pre-existing contacts

The automations only fire on new additions, so the 60 contacts already on the two lists would never have received a welcome. Four classic campaigns were built and sent on 26 Aug 2026 to cover them, mirroring the automation branches one-for-one so nobody got copy written for someone else:

# Campaign List Filter Sent Time
14 Welcome backfill · nl-BE · Leerkracht 2 AUDIENCE = Teacher 7 17:43
15 Welcome backfill · nl-BE · Ouder en Andere 2 AUDIENCE = Other, Parent 16 17:48
16 Welcome backfill · en-CA · Teacher 3 AUDIENCE = Teacher 17 17:53
17 Welcome backfill · en-CA · Parent and Other 3 AUDIENCE = Other, Parent 20 18:01

60 sent in total, 7 + 16 + 17 + 20, which is exactly the two lists’ combined membership — so the coverage is provably complete, not approximately complete. Final delivery across all four, read once #17 finished at 18:09: 59 of 60 delivered, zero hard bounces, one soft bounce (on #17), zero unsubscriptions, zero complaints. First opens arrived within five minutes; open rates settled at 12.5% (#15), 17.65% (#16) and 15.79% (#17), with one click on #17.

This is the first email either list has ever received. The cold-list-plus-cold-domain risk recorded on 24 Aug was real but is now spent: the domain was authenticated on 25 Aug first, and the first send went out to 60 opted-in contacts a day later.

Source: Brevo email_campaign_management_get_email_campaigns with globalStats, read 26 Aug 2026 18:03; recipient counts cross-checked against the Recipients panel before each send

Brevo campaigns do not need segments — filter the list by attribute instead

There are no segments in this account (segments_get_segments returns {}), and none are needed. A campaign’s Recipients panel has Advanced options → Filter recipients → Add a condition, which filters a chosen list by contact attribute at send time. All four backfill campaigns above used it, e.g. AUDIENCE is equal to [Other] [Parent] on list 3 → 20 recipients.

The filter UI resolves the AUDIENCE category ids to their labels, so it offers Other / Parent / Teacher rather than 3 / 1 / 2. Do not go looking for the numbers there.

A quirk in that panel: the condition’s attribute search box does not filter on a value that is pasted or typed all at once — the React input event never fires. Press BackSpace and retype the last character and the list filters correctly.

Source: built and sent four campaigns through this panel, 26 Aug 2026; segments_get_segments read the same day

Every campaign defaults to an unauthenticated sender and the wrong reply-to

A new campaign does not inherit the authenticated sender. It opens with “Team Wabi Sabi BV · decibellen@gmail.com” preselected and shows the warning “Your domain is not authenticated. To ensure delivery, we replace your domain with [@brevosend.com].” It must be switched to Verbomatique · hello@news.verbomatique.com by hand, on every campaign. The warning disappears once it is, which is the check that it took.

The same is true of reply-to, which defaults to decibellen@gmail.com and has to be set per campaign under Additional settings → Sending and Tracking. The locale split was applied to all four backfill campaigns: feedback@verbomatique.be on #14/#15, feedback@verbomatique.com on #16/#17.

Two defaults, both wrong, both silent, on every single campaign. This is the same class of trap as “Reply-To is per-campaign in Brevo” recorded on 25 Aug — the sender half had not been written down.

Source: observed on all four campaigns as they were created, 26 Aug 2026; final values confirmed from the API, which returned sender.email hello@news.verbomatique.com and the expected replyTo for each

The welcome email exists as eight separate copies, not two

The same four emails exist twice over, and editing one does not touch the other. Any copy change has to be applied to both members of each pair:

Locale · audience Template Automation-step message
en-CA · Parent and Other 6 11 (Automation #1, step #4)
en-CA · Teacher 7 10 (Automation #1, step #3)
nl-BE · Ouder en Andere 8 13 (Automation #2, step #5)
nl-BE · Leerkracht 9 12 (Automation #2, step #3)

The templates are what the one-off campaigns use; the automation-step messages are what the automations send. Both are reachable through templates_get_smtp_template, which is the only reliable way to confirm an edit actually saved.

Source: Brevo templates_get_smtp_templates and the two automation editors, 26 Aug 2026

Meta Lead Ads → Brevo sync (Pabbly Connect)

Site signup forms write to Brevo directly via the WordPress plugin. Meta Lead Ad forms do not — they need a bridge, and that bridge is Pabbly Connect (free tier, 100 tasks/month). Brevo has no usable native Facebook Lead Ads connector; Pabbly was the fallback after that route failed.

Workflows

Workflow Status Trigger (page / form) Brevo action
...Brevo." (en-CA) Live since Aug 10, 2026 Verbomatique.com / Verbomatique.com - Newsleter Signup Form-copy Create or Update Contact → Canada (English) Subscribers
...Brevo (nl-BE) Trigger only, action never saved Verbomatique secondaire / VS nl-BE Back to School - Nieuwsbrief

en-CA action config (the working reference)

  • Email ← res2 email, FIRST AND LASTNAME ← res2 full_name
  • List: Canada (English) Subscribers · Update Enabled: True
  • Email blacklisted: False · SMS blacklisted: False
  • AUDIENCE RADIO NL, CONSENT NL BE, AUDIENCE NL, LOCALE, LASTNAME, FIRSTNAME: all unset

Gotchas — read before touching this again

  • Field mapping requires a captured test response. With none, Pabbly’s / mapper says “No fields available in this step”, Email is mandatory, so the action step silently fails to save and the canvas reverts to trigger-only. There is no manual-token workaround worth trusting.
  • The nl-BE page has no associated Meta developer app, so developers.facebook.com/tools/lead-ads-testing refuses to create a test lead for it. The en-CA page does have one, which is why en-CA was buildable and nl-BE is not. Do not re-attempt the testing-tool route for nl-BE until the app exists.
  • The App Dashboard shows no “Create App” button, which almost certainly means the Facebook account has never completed developer registration.
  • Ads Manager’s lead-form preview can’t substitute — a draft campaign has no ad to preview.
  • The blacklist dropdowns default to displaying True on a freshly-added action step. True means the contact is blocked from receiving email. Always set both to False before saving. (Checked Aug 16: en-CA is correctly False; no leads were ever blacklisted.)
  • Polling triggers mark leads as seen. An active workflow with no action will consume incoming leads and write them nowhere — deactivate any half-built workflow.
  • Manual “Test Workflow” fetches only the most recent lead(s), not history. It cannot backfill.
  • The workflow only captures leads created after it was built. Anything from before Aug 10, 2026 (en-CA) was never synced and needs manual CSV import.

Manual fallback (no Pabbly)

Ads Manager → the Leads ad → Download leads, or Business Suite → All tools → Leads Center. Import into Brevo sorted by the audience column, in one pass per audience value, setting AUDIENCE NL and LOCALE as static values for the whole import. Adequate for campaigns producing leads in the low tens.


How leads actually arrive, and what the mapping drops

Meta leads arrive through Pabbly in a batch at roughly 02:15 CEST. They land with FIRST_AND_LASTNAME + LOCALE only — the static AUDIENCE and CONSENT_MARKETING mappings have never been added to the en-CA action. LOCALE was added and is confirmed working on every live lead since the backfill.

The gap is not Pabbly alone: of the 18 contacts missing both fields on 23 Aug, 13 had the Meta/Pabbly shape and 5 came from the CSV backfill, so the manual import route drops them too.

Known obstacle: Pabbly field mapping requires a captured test response. Without one the mapper reports “No fields available in this step”, Email is mandatory, and the action step silently fails to save, reverting the canvas to trigger-only. There is no manual-token workaround worth trusting. For nl-BE this was recorded as blocked behind generating a test lead, itself blocked behind Facebook developer registration. That was the wrong diagnosis. Pabbly refuses to capture a response unless the workflow is switched ON, and a trigger-only workflow left ON consumes leads — a catch-22 no number of test leads resolves. It was escaped on 25 Aug 2026 by toggling ON, capturing, and toggling OFF within a minute; the nl-BE action is now built, live and delivering. See “The real blocker was a catch-22 in Pabbly, not a missing test lead”. The separate claim that the nl-BE page has no associated Meta developer app was not re-tested and nothing here disproves it.

Source: Asana 1217542445377430, comment of 23 Aug 2026 08:15

The nl-BE workflow is LIVE — and PROVEN DELIVERING as of 26 Aug 2026

When New Lead in Facebook Lead Ads, Create or Update Contact in Brevo (nl-BE) — built 25 Aug, Active, and since verified to be doing its job. The nl-BE list has a working automated lead route for the first time since it was created on 17 July.

The whole chain was traced end to end on 26 Aug, rather than trusting Pabbly’s own success message:

  1. Meta Leads Centrum shows lead kirsten.fotovdw@gmail.com, Leadformulier-ID 1363649725367766, Verzonden op 25 Aug 2026 22:30, Ik ben... Ouder.
  2. Pabbly’s dashboard shows this workflow at 2 tasks consumed — it was 0 when built.
  3. Brevo holds contacts 65 and 66, created 26 Aug 02:08, listIds [2], CONSENT_MARKETING true, LOCALE nl-BE, first name only, no AUDIENCE — which is exactly this action’s saved configuration.

Action config, read live from the saved step on 26 Aug: Email ← res2 e-mailadres · FIRST AND LASTNAME ← res2 voornaam · LOCALE nl-BE (static) · CONSENT MARKETING True · List(s) België (Nederlands) Subscribers · Update Enabled True · email blacklisted False · sms blacklisted False · AUDIENCE None · LASTNAME and SMTP Blacklist Sender empty.

Trigger, same read: Facebook Page Verbomatique secondaire, Lead Ad Form VS nl-BE Back to School - Nieuwsbrief, Response A still captured.

Source: Pabbly Connect, Meta Business Suite and the Brevo API, all read 26 Aug 2026

The real blocker was a catch-22 in Pabbly, not a missing test lead

Pressing Save & Send Test Request on the trigger raises a blocking dialog — “You need to turn ON the workflow to capture the webhook response.” And a workflow turned ON with no action step is precisely what this file warns against, because its poller consumes incoming leads and writes them nowhere. Capturing a response required entering the one state the standing guidance forbids.

The escape, used successfully on 25 Aug: toggle ON → Save & Send Test Request → toggle OFF immediately → build and save the action offline with the captured response in hand → toggle ON for real. Two exposures of well under a minute rather than one lasting the whole build.

The captured response survives the toggle-off — confirmed 26 Aug: the trigger still shows “Response A”, and the action’s Email field still renders its captured sample value. That part is verified. The dialog’s exact wording is carried from the 25 Aug session and was deliberately not re-triggered, since doing so means pressing the button that starts the exposure.

The exposure is also smaller than it first reads: a lead consumed in that window is only marked seen by Pabbly. It remains in Meta for its full 90 days. The worst case is a lead that does not auto-sync, not a lost lead.

Whoever built this on 16 Aug hit that dialog and stopped, which was the right call. What was missing was not a test lead but a safe sequence.

Source: Cowork session of 25 Aug 2026 for the dialog; Pabbly re-read 26 Aug 2026 for the persistence

The Brevo action’s dangerous defaults — one of them undocumented

On a freshly added Brevo “Create or Update Contact” step:

Field Ships as Must be
Do you want to use email blacklisted? True False
Do you want to use sms blacklisted? True False
List(s) Canada (English) Subscribers the list you actually want

The blacklist trap was already recorded. The list default was not. It pre-selects the account’s other list — and saved unexamined, every nl-BE lead would have landed on the en-CA list and been blocked from receiving email, while the workflow reported success.

Verification note: the saved nl-BE values were confirmed correct on 26 Aug (both blacklists False, list = België). The defaults on a fresh step were not re-tested, because reproducing them means adding a new action step to a live workflow. Treat the defaults as asserted from 25 Aug and re-check them on the next workflow anyone builds.

Source: Cowork session of 25 Aug 2026; saved values re-read 26 Aug 2026

The nl-BE trigger payload is Dutch, and has no full name

The three form answers are named in Dutch — a consequence of the form being authored in Dutch, and a reason the en-CA mapping recipe does not transfer: res2 e-mailadres, res2 voornaam, res2 ik_ben... (value ouder / leerkracht, lowercase). The rest is ad metadata on the res1 prefix: created_time, id, ad_id, form_id, ad_name, adset_id, adset_name, campaign_id, campaign_name, is_organic, platform. res1 form_id reads 1363649725367766, matching the nl-BE form.

There is no full_name. The nl-BE form only ever asked for a first name, so FIRST_AND_LASTNAME can only be populated with a first name — unlike the en-CA action, which maps res2 full_name. Any CSV backfill for this list should use first names too, or the list ends up with two naming conventions and a campaign greeting that reads differently for each half. The 25 Aug backfill was re-cut to first names for exactly this reason.

Source: Pabbly trigger response panel, read field by field 26 Aug 2026

Free-tier consumption

100 tasks/month. 31 consumed, 69 remaining as of 26 Aug 2026 — 22 on 25 Aug, so the two live workflows are burning roughly ten a day between them at back-to-school volume. The en-CA workflow (built 10 Aug) accounts for 29 of the 31; the nl-BE one for 2. At this rate the ceiling is not close, but it is no longer irrelevant.

Source: Pabbly Connect dashboard, read 26 Aug 2026

The nl-BE form and its lead inventory (read 26 Aug 2026)

VS nl-BE Back to School - Nieuwsbrief, Formulierantwoorden ID 1363649725367766, Actief, created 16 Aug 2026 20:40. Forms library shows 23 leads, 0 verlopen.

That is 22 real leads plus one test submission. The test lead is worth knowing by sight: it displays in the table under the name “Dries Bellen”, dated 16 Aug, and only its detail panel gives it away — email test@meta.com, Verzonden op Zondag 16 augustus 2026 21:36, with Ik ben... and First name both reading <test lead: dummy data for ...>. Anyone filtering on the display name will miss it.

Of the 22 real leads, the 20 that existed on 25 Aug were imported to Brevo by CSV — 14 Ouder, 6 Leerkracht, confirmed by reading the AUDIENCE values back off contacts 45–64. The two newest (25 Aug) arrived automatically through Pabbly.

A second, older form also exists — “Verbomatique - Newsletter signup F…”, created 4 Aug 2026, 1 lead. It is not part of the nl-BE back-to-school route and nothing reads from it.

Source: Meta Business Suite forms library and Leadscentrum, read 26 Aug 2026; Brevo API same day

Leads Centrum is the readable route to lead data

Meta Business Suite → Leadscentrum had never been opened before 25 Aug; its first screen was a terms-of-service gate, accepted that day with Dries’ explicit authorisation. Confirmed 26 Aug: the gate is gone and the centre opens straight to data.

Tabelweergave renders every lead on screen, and clicking a row opens a Formulierantwoorden panel carrying the Leadformulier-ID, the “Verzonden op” timestamp, and every custom answer — so lead data is readable by a Claude session without a file download. The table paginates at 20; check for a second page before concluding a count. Each lead’s panel also shows its expiry inline as “Lead verloopt op " — 90 days out, so a 25 Aug lead reads 23 Nov.

The Page-context caveat. /latest/leads_center and /latest/instant_forms open on whichever Page the account last used, so an empty leads list is far more likely to mean wrong Page context than zero leads. Pin it: ?business_id=1159242368262681&asset_id=110739691610636. The 25 Aug note said the unpinned URL lands on jouwsportclub.be specifically; that did not reproduce on 26 Aug, but the test was worthless — the pinned URL had just been visited, so “last used” was already Verbomatique. Treat the mechanism as established and the specific wrong Page as an artefact of that day’s state.

Read-only rules for this surface: moving a lead between pipeline stages is an edit, as are “Lead verwijderen” and “Lead rapporteren”, both of which sit at the bottom of every detail panel.

Source: Meta Business Suite, read 26 Aug 2026

Advertising

  • Linked to GA4 property (verbomatique.com) as of Aug 15, 2026
  • Two conversion actions imported: click → Outbound click, form_submit → Sign-up
  • 22 Aug 2026: Search-BackToSchool-en_CA-2026 switched to optimise for Aanmeldingen (campaign-specific) instead of the account default Paginaweergaven. The underlying action is the GA4 form_submit import from property 548294070.
  • This did not fix conversion reporting, and cannot. form_submit effectively never fires — twice in 28 days, from a single user, against 92 outbound store-badge clicks. A campaign optimising for an event that does not occur will report zero conversions indefinitely. This is not a GA4 plumbing fault; do not send anyone to debug the GA4 import. If a usable conversion signal is wanted here, it has to be click (outbound), or the form has to start converting.
  • Note the bid strategy (Klikken maximaliseren) does not consume conversion data, so this affects reporting and attribution, not bidding.
  • A Leads van telefoongesprekken conversion action still exists although neither site has a phone number. Leftover from old Display/Smart campaigns; flag once for removal, then ignore.
  • Account currency: EUR · Account language: Dutch (interface shows in Dutch)
  • Decimal separator is comma, not period (€6,91 not €6.91)

Campaigns: | Campaign | Status | Budget | Targeting | Dates | |—|—|—|—|—| | Search-Herexamens-nl_BE-2026 | Active | €6/day | Belgium (Brussels), Dutch | Jul 24 – Aug 27, 2026 | | Search-BackToSchool-en_CA-2026 | Active — delivering | €6.91/day | Canada — no Québec exclusion configured, English | Aug 15 – Sep 14, 2026 |

Meta Ads Account: 263136948126381

  • Pixel ID: 2835951779840335 (“verbomatique pixel”), linked to verbomatique.com
  • Custom event PlayStoreClick fires on store badge clicks (manually configured, working)
  • Enhanced Measurement events auto-fire (PageView, SubscribedButtonClick on clicks)
  • Account has been running awareness campaign since mid-July 2026

Current Meta ads (running):

  • Traffic Ad: “French conjugation, without the fight.” — €0.19/landing page view, 4,120 impressions
  • Lead Ad: “French verb drills your teen won’t fight you on” — €3.81/lead, 11 leads

nl-BE back-to-school campaigns — LIVE since 19 Aug 2026

Both published 19 Aug, running 20–31 Aug 2026. Flemish schools start Tue 1 Sept.

Campaign Creative Budget Copy
VS nl-BE Back to School — Leads Score report, 4/6, two visible mistakes with corrections Campaign level “De app houdt de fouten bij. Niet jij.” · Kop: “Franse werkwoorden die nog niet goed zitten” · CTA Registreren
VS nl-BE Back to School — Verkeer Afdekken screen, three pronouns answered, three blank €4.00/day (raised from €1.00) “Franse vervoegingen. Zonder ruzie.” · Kop: “Huiswerk Frans begint weer. De ruzies hoeven niet.” · CTA Download

The €4/day was forced, not chosen. On publish Meta warned “Je campagne behaalt mogelijk 0 resultaten met je huidige budget” and named €4.00/day as the minimum for ~1 result/day. At €1 the Verkeer ad set likely would not have delivered. ~€48 over the flight.

Advantage+ creative enhancements: four OFF, one ON. Visuele retouchering, Overlays toevoegen, Tekstverbeteringen and Animatie toevoegen all default to ON and would have altered a deliberately composed 1080×1080 layout — Meta adding its own text over an ad that already has a headline. Turned off. Muziek left on. Campagnescore dropped 81 → 67 as a result; that is Meta’s optimisation score, not a quality judgement, and the trade was made knowingly. Meta remembers these per account — the second ad already had them off.

Creative production: screenshots captured from a real Android emulator at 1080×2424, nl-BE locale, verb aller, via an integration-test harness. Composed to exactly 1080×1080. An earlier Verkeer creative was rejected by Meta as “Media niet breed genoeg” (under 500px wide); resolved. Only remaining warning on both ads is the harmless Audience Network Rewarded Video notice, which requires video.

The Verkeer ad also carries an Expresformulier lead-form extension, so it can generate leads as well as traffic. Decide whether that is wanted.

Back-to-school ads (prepared, not yet uploaded):

  • Variant A: “French homework starts again. The fights don’t have to.”
  • Variant B: “Tired of ads interrupting homework? Ad-free for CAD $1.99.”
  • Both produced by Claude Design at 1080×1080, matching existing style

Reddit Ads

  • Account created Aug 6, 2026. A brand-awareness campaign went live same day.
  • Rep contact: Rati Agarwal (v.rati.agarwal@reddit.com), CC: Samuel Richard
  • Reply sent Aug 15 accepting optimization help, asking about Belgium viability, requesting call slots in CET hours
  • Rep as of 24 Aug 2026: Ussama Sayeed (Client Partner, SMBs EMEA, v.ussama.sayeed@reddit.com). See the Reddit — platform guidance section.

First flight (6–18 Aug 2026, 13 complete days): €85.59 spend, 30,982 impressions, 165 clicks, €2.76 eCPM, €0.52 CPC, 0.533% CTR, €6.58 average daily spend. Account created 5 Aug; first delivery 6 Aug. Paused 19 Aug.

Relaunch campaign 2567099998208211794 — paused, €43.72 of a €100 cap spent, €56.28 of cap headroom never used. It will not be used: Reddit advertising was discontinued on 26 Aug 2026. Leave the campaign paused; do not reactivate, rename or archive it.

Re-corrected 25 Aug 2026. The 24 Aug note here said €56.28 was “no longer the budget September runs on” because a €500 credit had appeared. That credit does not exist — see the corrected block below. €43.72 spent against a €100.00 cap was confirmed directly in Ads Manager on 25 Aug, so €56.28 is the accurate remainder. But it is cap headroom, not a budget: it is how much more this paused campaign is permitted to spend, charged to the card, not money held on the account. (Asana 1217796325932409)

Corrected 24 Aug 2026. This previously read “Paused to bank the remaining ~€13 for the Sept 1–25 back-to-school window.” That figure was wrong: it came from reading an account-level lifetime total as a single campaign’s spend. The reasoning behind the pause still stands — off-season spend buys less than the same money in September.

Reddit warned on pause that the campaign was “still learning” and recommends 14 days minimum — we stopped at day 13. The standing question for the rep — does relaunching restart the learning phase from scratch, or does reactivating preserve it? — was never answered and is moot since 26 Aug 2026: advertising is discontinued, so there is no rebuild-vs-resume decision to make. (Partly answered 24 Aug: renaming a campaign or ad group resets the learning phase at both levels — see Reddit — platform guidance.)

There is no Reddit conversion pixel, and there will not be one. Reddit traffic does not auto-tag, so it landed in GA4 as Unassigned and its conversions were invisible in both directions. With advertising discontinued on 26 Aug 2026 this is now permanent: the €86.72 spent bought 168 clicks whose value can never be established. If Reddit ads ever resume, install the pixel and UTMs before the first euro, not during.

AdMob revenue baseline (19 Jul – 17 Aug 2026, pre-campaign)

Segment Revenue eCPM Impressions
Canada iOS €0.28 €6.41 44
Belgium iOS €0.22 €3.96 56
Canada Android €0.13 €2.67 49
Belgium Android €0.07 €1.21 57
NL/UK/FR/MA/BR/other €0.05 29
Total €0.75 €3.21 235

Match rate 92.96% overall; 341 requests.

Read this correctly. The eCPMs are healthy — Canada iOS at €6.41 is a good rate. What’s missing is traffic, not monetisation quality. This is a volume problem and marketing has been limited so far, which is why the ad-supported tier is being kept rather than removed.

Consequence: Belgium is ~€3.50/year. A blanket tagForUnderAgeOfConsent costs €0.70–€1.40/year, so it is not a revenue decision at this scale. Revisit only when Belgian ad revenue passes ~€10/month, at which point conditional TFUA driven by the real consent signal becomes worth the extra work.

WordPress.com Blaze — $200 credit

Verified 24 Aug 2026 in Tools → Advertising on verbomatique.com.

  • US$200.00, unspent, expiring 1 Aug 2027. Zero campaigns, zero payments, 0 ad views, 0 clicks.
  • Site-scoped to verbomatique.com. The nl-BE site is self-hosted with no Jetpack, so it cannot use this.
  • Inventory: WordPress.com blogs + Tumblr. CPM model.
  • Targeting: language, location down to province (include-only — no exclusion control, so Quebec is handled by never adding it), interest category, device. No age or parent demographic targeting.
  • Only two promotable items exist (Cookie Policy, Hello World!) because the site is a single-page build — but Advanced Settings → Destination URL offers “The site home”, and a real page can be promoted directly once it exists.
  • URL parameters field exists (free text, 2096 chars) — UTMs are supported.
  • Trap: schedule defaults to “Run until you cancel it” with recurring weekly charges to the saved card once credits are exhausted. Use “Specify the duration”.
  • Estimate at US$5/day: 29.8k–40.3k weekly ad views, 67–91 weekly clicks (worldwide/all-topics — narrowing will change it).

Blaze creative background — DECIDED 24 Aug 2026

Cream field #FAF9F5 plus a full-bleed 16–20px border in --vb-teal #0B5443, identical on all three variants. Cream-on-white has no edge against WordPress/Tumblr page backgrounds.

This means the Branding section’s claim that ad creative style is “consistent across Meta, site, and print materials” is no longer true. Blaze diverges deliberately. Whether the border comes back to Meta creative is undecided.

Known gap in this file — CLOSED 25 Aug 2026

The gap recorded here was the unverified claim that €43.01 of historical Reddit spend sat in an ended flight 2562145456864748615. Both halves are correct. Read from Ads Manager on 25 Aug 2026: campaign 2562145456864748615 has spent exactly €43.01 (17,096 impressions, 92 clicks) and reads “Not delivering — Campaign Schedule ended”, its ad group 2562145456864828991 scheduled Aug 5 to Aug 12 2026.

The recommendation attached to it — rename or archive — should not be acted on: renaming resets the learning phase at both levels (Ussama, 24 Aug). Leave it alone.

The dropped sentence was right; leaving it out until it could be checked was still the correct call.


Reddit: the “€500 ad credit” was never a credit — CORRECTED 25 Aug 2026

This section previously stated that a €500 ad credit existed on the account. It does not, and did not. Ussama Sayeed’s email of 24 Aug 2026, 12:42 (“Verbomatique x Reddit”) described it as a credit “to check for and activate”; the account itself shows an empty Ad Credit panel and an unclaimed spend-matching offer — €500 in credit when you spend €500. See “Reddit: the €500 is a spend-matching offer, not a credit” below for the verified terms, and note that the offer was deliberately not claimed on 25 Aug.

Ussama’s verbal recommendation on the call was €50–100/day, on a cold-audience argument. It was worth weighing while the €500 looked free; it is not free, so treat the recommendation as what it always was — a vendor advising more spend.

The lesson worth keeping: a figure quoted by a platform’s own sales representative is a claim, not a reading. This one travelled through four documents and reshaped a whole month’s plan before anyone opened the billing page. Verify money in the account that holds it.

Source: Asana 1217796325932409, created 24 Aug 2026 14:58, corrected by the comment of 25 Aug 2026

Reddit: renaming a live campaign resets the learning phase — build new instead

Confirmed by Ussama Sayeed on the 24 Aug call: renaming causes a learning-phase reset at both campaign and ad-group level. This inverts the standing plan, which was to rename the badly-named existing entities so learning would survive.

Current state of the account, read from the Ad Groups tab with View Setup on, 24 Aug: both campaigns are named “VS EN-CA Launch - Reddit Traffic” and both ad groups (2562145456864828991 and 2567099998209021441) are named “Asset Group 1”. Both ad groups also carry identical targeting — Canada with no exclusions; communities Teachers / languagelearning / alberta / French / ontario / britishcolumbia / Parenting; keywords french conjugation, french grammar, french homework help, language learning apps, study tools.

The resolution: do not rename anything. The plan was to build the September flight as new campaigns with correct names, since a new campaign enters its learning phase regardless. That flight was cancelled on 26 Aug 2026, but the do-not-rename instruction outlives it: campaign 2567099998208211794 — paused 19 Aug at day 13 of a recommended 14 while Reddit still flagged it “still learning” — keeps whatever partial learning it holds only for as long as nobody renames it. Leave both campaigns exactly as they are.

Never answered by Ussama: whether reactivating a paused campaign preserves its partial learning or restarts it. Moot since 26 Aug 2026 — nothing will be reactivated.

Source: Asana 1217633033987496, comments of 24 Aug 2026 07:59 and 14:59

Reddit: the cold-creative benchmark to beat

The 13-day cold-creative flight produced €0,52 CPC, 0.533% CTR, 165 clicks. That was the number a promoted-organic test would have had to beat. No such test will run — advertising was discontinued 26 Aug 2026 — so this stands as the channel’s closing cost record, not a target.

Source: Asana 1217633033987496, comment of 24 Aug 2026 17:11

Cost per store visit by channel, 18–24 Aug 2026

Google Search en-CA €7,06 per store visit vs Meta Traffic €12,45. Google Search is the best-performing channel per outcome on this evidence.

Search-BackToSchool-en_CA-2026 over that window: 4.831 impressions, 63 clicks, 1.30% CTR, €1,12 average CPC, €70,61 spent, 0 conversions. GA4 google/cpc for the same window: 61 sessions, 10 key events — a 9.84% key-event rate, the best of any channel.

Worth carrying forward because it is easy to misread: essentially all of this campaign’s lifetime delivery happened in that one week. Lifetime 25 Jul–23 Aug was 4.296 impressions / 59 clicks; 18–24 Aug alone was 4.831 / 63. There is no declining-CTR trend to read — the 1.69% / 1.37% / 1.30% readings are the same week measured at different cut-offs on a base that started at zero.

Source: Asana 1217582438101941, comment of 24 Aug 2026 07:59

Keyword Planner, Canada — the volumes that decide the en-CA rebuild (verified 24 Aug 2026)

Average monthly searches, Canada, with competition and top-of-page bid range:

Term Vol/mo Comp Bid range
conjugate french verbs 8.100 Low €0,64–3,62
french conjugation practice 880 Low €0,73–3,22
practice french verbs 320 Low €0,81–3,91
french conjugation app 50 Low €1,03–1,44
french homework help 50 Low
french verb conjugation app 30 Low (+300% YoY)
french immersion resources 30 High €0,42–2,69
french verb app 10 Medium
best french conjugation app 10 Low
french immersion homework help 10 Low
app to learn french verbs / french immersion practice / core french practice no data

conjugate french verbs is 16× the entire app cluster and cheaper than the account’s €1,12 average CPC. Its trend peaks near 14K in September and troughs in July — the −45% three-month change is summer holidays, not decline.

The French-immersion angle is dead — 10–50 searches/month across every variant, and the only term with real competition is 30/mo and almost certainly school boards. Do not revisit without a Canadian-curriculum landing page.

The structural diagnosis behind this: the campaign buys reference intent while the product serves practice intent. Queries containing practice / conjugate / app / online convert; grammar / tenses / passe compose / prepositions do not. On 18–24 Aug the nine largest reference queries produced 381 impressions and 1 click.

Sources: Asana 1217582438101941, comments of 24 Aug 2026 08:16 and 08:37; Asana 1217775004294568 (created 24 Aug 2026 08:37)

A pre-existing broad negative on “free” is blocking qualified traffic

Search-BackToSchool-en_CA-2026 carries pre-existing broad negatives: dictionary, free, italian, jobs, spanish, translator. “free” is broad match, so it blocks any query containing it — including “free french conjugation app” — while the site’s own positioning is “Free to start. No card required.” Left in place on 24 Aug only because removing an existing negative was out of scope. Needs a decision.

Sixteen phrase-match negatives were added at campaign level on 24 Aug (passe compose, passé composé, prepositions, grammar rules, for beginners, tenses, etre conjugation, avoir conjugation, translation, translate, meaning, definition, worksheet, pdf, bescherelle, conjugaison), bringing the campaign to 22 negatives total. If clicks fall materially, “tenses” is the one most likely over-broad — it also blocks “french verb tenses practice”.

Source: Asana 1217582438101941, comment of 24 Aug 2026 08:37

nl-BE Search has the account’s best CTR and is capped by budget

Search-BackToSchool-nl_BE-2026, all-time as read from the platform 24 Aug 2026: 932 impressions, 57 clicks, 6,12% CTR, €0,71 average CPC, €40,45 spent, 0,00 conversions. Google Ads explicitly flags it “Beperkt door budget” at €6,00/day, and the last two days ran roughly €9/day against that €6,00 ceiling.

Not a small-sample artefact: against the 22 Aug baseline (478 impressions, 29 clicks, €22,33) the increment is 454 impressions and 28 clicks — 6,17% CTR on the new traffic, 4,7× the en-CA campaign’s 1,30%.

The nl-BE keywords (“frans oefenen”, “werkwoorden frans oefenen”) run 4,44–12,00% CTR at €0,49–0,73 CPC because they are practice-intent by construction. The en-CA ones are not. That is the whole difference.

Source: Asana 1217775026626294 (created 24 Aug 2026 09:31); nl-BE/en-CA CTR comparison from Asana 1217582438101941, comment of 24 Aug 2026 08:16

Quebec exclusion — the real state per channel (re-confirmed 24 Aug 2026)

  • Meta — does exclude Quebec. Ad sets are named “Parents CA (excl. QC) 30-55”.
  • Reddit — targets Canada with no exclusion. This is intentional and correct; an earlier brief wrongly claimed otherwise. Do not “fix” it.
  • Google Ads Search-BackToSchool-en_CA-2026 — no Quebec exclusion configured.

Source: Asana 1217776432181383 (created 24 Aug 2026 08:02)

Blaze — operational limits beyond the schedule trap

  • Minimum daily budget is US$5 per campaign. Three concurrent campaigns = $15/day, so 12 days × $15 = $180 against the $200 credit with a $20 buffer.
  • The recurring-charge consent commits against a saved Mastercard ending 7400 once credits are exhausted.
  • Blaze is CPM and the daily budget is a ceiling, not a commitment — actual spend often comes in under, and leftover credit funds a second flight.
  • Location targeting is include-only at state/province level. The nine provinces to add individually: Ontario, British Columbia, Alberta, Manitoba, Nova Scotia, New Brunswick, Saskatchewan, Prince Edward Island, Newfoundland and Labrador. Quebec is handled by never adding it.
  • Proof that no charge occurred is the Payments tab still showing no payments — not the absence of an email.

Source: Asana 1217802736470180 (created 24 Aug 2026 15:37)

Blaze pre-fills AI-generated ad copy, and it overclaims

Observed 24 Aug 2026 when opening the wizard on post 173. Blaze had already written, into the live fields:

Field Blaze’s default
Title “Stop homework fights”
Snippet “Your child can master French verbs solo in 10 minutes—no nagging, no stress, just results.”
CTA “Try it free now”

“Master French verbs solo in 10 minutes” is a performance promise the app does not make. Overwrite all three text fields on every campaign — the defaults are pre-populated, not placeholders, so they ship if left alone.

Two smaller wizard facts from the same session:

  • The Duration field is a controlled component and ignores scripted input. Use the + button; the default is 7.
  • The wizard does not save drafts. A reload loses a part-built campaign entirely — an attempt on 24 Aug got campaign A roughly 80% built before the browser connector dropped, and all of it was lost. Build one campaign per sitting.

Source: Asana 1217801744397059 (created 24 Aug 2026 19:41), which records the snippet verbatim, the overwrite rule and both wizard facts. The Title and CTA strings are from the same 24 Aug session as reported by Dries; they are not quoted in that task.

Meta en-CA lead cost, 18–24 Aug 2026

Meta produced 6 leads in the six days to 24 Aug at €4,46 each.

Source: Asana 1217542445377430, comment of 24 Aug 2026 07:59

Organic social — the shape decided 25 Aug 2026

Meta ads were producing Instagram followers who landed on an effectively empty profile. The plan written that day is claude/verbomatique-social-media-plan.md; the decisions worth keeping outside it:

  • The profile is a trust checkpoint inside the ad funnel, not a traffic channel. At two posts a week from a young account, organic reach will not be a meaningful source this autumn. Nearly all the available value is in the one-off fix — bio, link, nine seed posts — so that ad traffic tapping the account name finds something.
  • Market split by platform, not by account: one Instagram account carries en-CA, the Facebook Page carries nl-BE. Same asset, swapped caption. This is a judgement, not a measurement; Business Suite → Insights → follower demographics settles it.
  • Content is product-led on every card — practice modes, score report, no-account/offline, pricing, brand line. Chosen over generic French-teaching content, which travels further but sells nothing.
  • The product deck is finite: five cards × three headlines × two markets ≈ fifteen weeks. New screenshots refill it; conjugation cards are the reserve behind that.
  • Kill criterion, set in advance: review 20 Oct. Fewer than three outbound store clicks a month from organic social → stop posting, keep the profile as a static shopfront. The bar is Google Search at €7,06 per store visit.
  • Engagement Custom Audiences (Instagram + Facebook, 365-day window) should be created now regardless — the window only accrues from the moment the audience exists.

Neither site links to any social profile. Verified 25 Aug 2026 by fetching both live public pages and reading every link including footers: store badges and a cookie policy only. The verbomatique.com footer is still the theme placeholder, so there is an obvious slot.

Handle discipline for the rebrand: never register a handle containing “secondaire”. Instagram usernames can be changed but every saved screenshot, tag and link breaks; Facebook Page name changes go through review.

Source: Cowork session of 25 Aug 2026; site link check by public fetch the same day

The app icon is a wide mark, not a square tile

The logo supplied on 25 Aug is 523×355 — a rounded-rectangle robot face with a French tricolour visor, on an opaque white background, in the blue family (--vb-blue and a brighter blue), not the teal/coral of the card system. Two consequences for any creative work:

  • It must be contained at its own aspect, never cropped into a square app tile.
  • On the cream field the outer white has to be keyed out or it reads as a white plaque. The white stripe inside the flag must survive that — key from the border inwards, not by global colour replacement.

Worth a deliberate decision at some point: the logo is blue-based while the card and site system is teal and coral. This file already treats --vb-blue as “logo frame”, so the split is longstanding, but it becomes visible once the icon appears on every marketing card.

Source: Cowork session of 25 Aug 2026, working from the file supplied by Dries

Reddit: the €500 is a spend-matching offer, not a credit — verified 25 Aug 2026

There is no €500 credit on the account, and there never was one. Read directly from Ads Manager → Billing → Overview on 25 Aug 2026: the Ad Credit panel is empty. It contains an “Add promo code” link and nothing else — no balance, no expiry line. Credit Card Balance €3.13, billing threshold €10.00, Mastercard **7400 on file, business location BE, currency EUR.

What exists is an unclaimed promotional offer, surfaced as a green “Get €500 in ad credit” pill in the Ads Manager top bar. Opening it produces a modal reading “Get €500 in ad credit when you spend €500!”, with this fine print, near-verbatim:

Offer valid through 9/4/2026. Ad credits expire 14 days after receipt. To earn ad credits, payment for the minimum amount stated in the offer must be made to Reddit within 14 days. Limit one per account, and non-transferable.

The real shape of the deal is two clocks and €500 of our own cash:

  1. Claim the offer (by ~4 Sept — Reddit’s UI is US-formatted and 9 April is already past, so 4 September is the only consistent reading; this is an inference from date format, not something the UI spells out).
  2. Pay Reddit €500 of real money within 14 days of claiming. Spend before the claim does not count toward progress.
  3. The €500 credit is then issued, typically within 24 hours, and expires 14 days after issue.

So taking it means roughly €1,000 of delivery inside about four weeks, half of it cash out of pocket, against an account whose entire lifetime spend is €86.72.

Claiming is effectively irreversible. Per Reddit’s Reddit Ads Credits help article (linked from the modal as “Additional terms apply”): once an offer is accepted, an advertiser cannot claim another one unless it expires or all credits are used. Other constraints from the same article: one active promo per Ads Account, non-transferable, cannot be split across accounts or moved between them, only the account owner may accept, an expired promo cannot be reinstated, offers are void where prohibited and may be revoked at any time and for any reason by Reddit.

How it interacts with the €100 cap: it does not, directly. Ads Credits are account-level and auto-apply to spend accrued after issuance; they never clear an outstanding balance from before. The €100 figure is a campaign spend cap on 2567099998208211794 and would still throttle that campaign independently.

Decision, 25 Aug 2026: not claimed. Dries declined, on the grounds that claiming locks out any other promo and commits to spend the project has not decided to make.

Note the tension in the vendor’s own advice. The same rep who asked us to “check for and activate the €500 ad credit” also wrote, by email on 24 Aug, that “a minimum spend of €3,000 is required to gather sufficient data and achieve actual results during the learning phase” and otherwise recommended organic only. Read together with what the offer actually is, the €500 reads as a spend incentive rather than a gift.

Source: Reddit Ads Manager account jgqslt6433w2, Billing page and offer modal, read 25 Aug 2026; business.reddithelp.com “Reddit Ads Credits” article, same date; Asana 1217796325932409, comment of 25 Aug 2026

Reddit: the account’s real lifetime numbers, per campaign (read 25 Aug 2026)

Dashboard, Jan 1 – Aug 25 2026, campaign view with View Setup on:

Campaign Status Impressions Clicks Spend Cap
2567099998208211794 Inactive — campaign paused 14,083 76 €43.72 €100.00
2562145456864748615 On, but not delivering — campaign schedule ended 17,096 92 €43.01 none
Account total   31,179 168 €86.72  

Account-level: €0.52 CPC, €2.78 eCPM, 0.539% CTR.

This settles what “€56,28” is. €100.00 cap − €43.72 spent = €56.28. It is headroom under a campaign spend cap — how much more that one paused campaign is permitted to spend before Reddit stops it — not a credit, not a balance, and not money Reddit owes us. Every euro of it would be charged to the Mastercard on file. The figure has been carried through several documents as though it were available funds.

Campaign 2562145456864748615 is toggled ON but cannot spend. Its ad group 2562145456864828991 reads “Not delivering — Campaign Schedule ended”, with start Aug 5 and end Aug 12 2026. Harmless, but it reads as a live campaign in the campaign list.

Source: Reddit Ads Manager account jgqslt6433w2, dashboard read 25 Aug 2026

Reddit: the September flight re-plan — SUPERSEDED 26 Aug 2026

No September flight will run. Reddit advertising was discontinued on 26 Aug 2026 — see “Reddit advertising discontinued” in Advertising. The planning below is kept as the record of what was considered, including the unresolved €4/day-vs-€5/day question, which no longer needs settling.

With the €500 declined, the September window is funded from the card. Two constraints bind, and neither is the money:

  • The learning phase. Ussama’s guidance is 14 days minimum; the August flight was paused at day 13 while Reddit still flagged it “still learning”. A flight shorter than the learning phase repeats that mistake and buys nothing durable.
  • The pixel. There is still no Reddit conversion pixel (Asana 1217633136866788). Any spend without it buys clicks at a known €0.52 and teaches nothing past the click — which is exactly what the €86.72 already spent has taught. The pixel, not the budget, is the gate on the September flight.

Reactivating the paused campaign to consume its €56.28 headroom is the weakest path. At any plausible daily rate it funds well under 14 days, and it is still unknown whether reactivating a paused campaign preserves its partial learning or restarts it — Ussama was asked on 24 Aug and did not answer. Building new (consistent with the 24 Aug do-not-rename decision) and funding a full learning phase is the cleaner call.

Reddit’s minimum daily budget is not established. Two different unverified figures are in circulation — €4/day in the 24 Aug rep-call brief, €5/day in the 25 Aug P7 re-plan — and they size the flight differently. Settle it in the campaign creation wizard before committing to a flight length. (Do not borrow the Blaze figure; that is a separate platform.)

Source: Asana 1217633033987496 and 1217542445406771, comments of 25 Aug 2026

Reddit advertising discontinued — decided 26 Aug 2026

Dries decided on 26 Aug 2026 to stop advertising on Reddit once the currently allocated budget runs out. No September flight will be funded and no further Reddit ad spend will be committed.

The rationale is the learning-phase floor. Reddit’s own rep put it at €3,000, quoted in Asana 1217633033987496 (comment of 24 Aug 2026) from Ussama Sayeed’s second email: “Our data shows that a minimum spend of €3,000 is required to gather sufficient data and achieve actual results during the learning phase. If you plan to commit to that budget level, we can certainly start working on setting up the new campaigns. Otherwise, I would recommend focusing exclusively on organic reach for the moment.” €3,000 is not a sum Dries is willing to spend now, and every flight length previously costed in this file — €70, €100, €125 — is between a twentieth and a twenty-fifth of that floor, so it buys an unoptimised campaign. That is what the €86.72 already spent bought.

In practice the decision takes effect immediately, because nothing is spending. On the 25 Aug reads recorded elsewhere in this file, campaign 2567099998208211794 is paused at €43.72 against a €100.00 cap, and campaign 2562145456864748615 is toggled on but its schedule ended 12 Aug, so it cannot deliver. There has been no Reddit spend since 19 Aug. The “€56.28” is cap headroom on a paused campaign, not funds. So there is no allocated budget still running down: nothing needs to be stopped, only left stopped. Ads Manager was not re-opened on 26 Aug — this rests on the 25 Aug reads plus the absence of action since.

Leave the account exactly as it is. Do not reactivate, do not rename, do not archive. Renaming resets the learning phase at both campaign and ad-group level (Ussama, 24 Aug), and the duplicate names are now cosmetic clutter in an account nobody will report on again.

Final channel numbers. Reddit’s lifetime total is €86.72 for 31,179 impressions and 168 clicks — €0.52 CPC, €2.78 eCPM, 0.539% CTR — and, because the pixel was never installed, no outcome figure at all. Reddit is therefore not rankable against Google Search (€7,06 per store visit) or Meta Traffic (€12,45), and never will be. Treat €86.72 for 168 clicks of unmeasured value as the closing figure for the channel, not an interim one.

Organic Reddit is unaffected. The rep’s own advice was to focus exclusively on organic reach, and the listening exercise (Asana 1217639836805570) stays open and undated.

Source: Dries, 26 Aug 2026; Asana 1217542445406771, 1217633136866788, 1217633033987496 and 1217632914852105, all closed 26 Aug 2026 with the decision recorded on each

Reddit — platform guidance

Source: 2026_Creative_Best_Practices.pdf and Reddit_Pros_organic_playbook.pdf, sent by Ussama Sayeed (Client Partner, SMBs EMEA, v.ussama.sayeed@reddit.com) on 24 Aug 2026.

Read this before using any figure below

Study population: ~144k in-feed ads from ~7k US advertisers over 12 months. Eligibility required English-language ads with conversion tracking enabled. Verbomatique is Belgian-run, targets Canada, and has no Reddit pixel — so these findings describe a cohort we are not in.

Reddit’s own caveat, verbatim: “these aren’t rules. Just learnings.”

Most of it is correlational. Advertisers who add logos, run three formats and vary their headlines are probably better funded and more sophisticated overall. Treat aspect ratio and headline length as the most trustworthy — they’re cheap, specific, and mechanically plausible. Treat the multi-format multipliers as the least.

Creative — image and video

Aspect ratio, average conversion rate impact

Image: 4:5 = +9.7% · 1:1 = +1.3% · 4:3 = −0.9% · 16:9 = −30.7% Video: 4:5 = +14% · 4:3 = +4.7% · 1:1 = +1.6% · 16:9 = −11.9%

4:5 can produce black bars on desktop. Reddit suggests 4:3 if desktop appearance matters.

Video length: 1–6 sec = +16.8% · 6–10 sec = +11.3% · 10–15 sec = +1.7%. Keep under 10 seconds. The last frame with the replay button may be all anyone sees — design the end card like an image ad.

Logos: images with a logo saw 108% higher average conversion than those without. Placement grid (nine positions):

  left centre right
top +29.1% 23.5% <10%
middle <10% 10.2% <10%
bottom 13.2% 24.9% +28.7%

Video: logo in the first 3 seconds = +6.6%; final 3 seconds = +9.3%.

Faces in images: +18.5%. Lifestyle backgrounds with low contrast/brightness outperformed solid bright, saturated backgrounds. Videos with low brightness: +11.7%.

Overlay text — the direction differs by medium. On IMAGES it lowers conversion by an average of 13%. The exception is urgency: mention of a season = +16%, plus ALL CAPS, time frame, discount language, call to action. On VIDEO it raises conversion by 8.2%; overlay using “Sale” or “Save” = >10%. Include closed captions — video plays with sound off by default.

Format variety: 2+ formats = 2.5× vs Reddit baseline; 3+ = 3.1×. Reddit suggests uploading a video as a looping GIF for extra format density.

Conversation Ads placement differs from in-feed: headlines under 100 characters, 4:3 aspect, remove overlay text and logos, prioritise visual clarity, be authentic (people, low colour and saturation).

Creative — headlines

On Reddit users see the headline before the asset, and everyone knows it’s an ad. Say who you are, what you’re selling, why they should care.

  • Under 150 characters: +8–25%. Under 50 drove the highest conversion.
  • Leading with a question: +9.3%
  • Mentioning the brand name: +6.5% conversion, −6.5% CPA
  • At least 1-in-3 headlines unique across a campaign: +12.4%
  • Use first and second person (we, our, you, your)
  • Hero the brand name; highlight price points and discounts; mention a relevant time frame, season or moment; include a simple CTA
  • Read headlines aloud — they should sound like a person, not a brand

Organic — Reddit Pro

Reddit Pro is free: business.reddit.com/pro

Platform context from the playbook: 110M daily active unique visitors, 21% YoY growth (Reddit Earnings Q2 2025). 51% of total online mentions of purchasing discussions happen on Reddit (Brandwatch, Oct 2023 – Mar 2024). Reddit is the #1 most-cited source across ChatGPT, Perplexity and other AI per Profound.

Three phases, in order. Do not skip ahead.

1. Lurk — track and listen. Guiding line: “you are not here to market, you are here to help.” Use the Trends tab. Add keywords for business name, product names, category, competitors, and trends worth watching (max 5 smart keywords). Then: find which communities mention them, read top conversations to see what problems the audience is solving, identify where your expertise adds value.

2. Comment. The first step for a business. Find conversations via Pro, review community rules first, answer straightforward questions, give expert advice. Listen before speaking; don’t force it. Keep it casual — write as if talking to close colleagues. Be a person, not a brand. Always reply back. Do not sell — most communities forbid self-promotion.

3. Post — only after commenting has built credibility. Contact mods via ModMail before posting, especially in a subreddit dedicated to your category. Use post check. Timing, quality, relevance and luck all matter.

Do: listen; read the room; start where your brand is already discussed; communicate as an individual; comment frequently, post sparingly; consider made-for-Reddit content such as AMAs; get moderator blessing before posting.

Don’t: be self-promotional; spam; antagonise; use corporate speak; leave a redditor hanging; post in a community you don’t own without talking to mods first (comments are fine); panic when something doesn’t land.

Username options: business (u/reddit), employee (u/brandy_at_reddit), or role (u/Reddit_engineering).

Organic — what to measure

The playbook is explicit about the order, and it’s the opposite of how paid campaigns are usually judged:

  1. Content quality first. Upvote rate — 75%+ is best in class for businesses. Are you getting comments back? Total account upvotes and comments should grow over time.
  2. Growth second — account-level reach, only once content is resonating.
  3. Brand awareness and traffic long term — brand mention volume, share of voice, profile followers, all trackable in Pro.

The bridge from organic to paid

Reddit’s own recommended route, and the one that suits a small budget:

  1. Reddit Pro performance tab → find your best-performing organic post
  2. Ads Manager → when creating the campaign, choose creative from the “Post Library”
  3. Promote that post as an ad

The creative is pre-validated by actual redditors, native by construction, and free to produce.

Also flagged: run an AMA with someone the audience wants to hear from, and promote it with AMA ads before, during and after.

Budget threshold — stated by Reddit, 24 Aug 2026

Ussama Sayeed, by email, verbatim: “Our data shows that a minimum spend of €3,000 is required to gather sufficient data and achieve actual results during the learning phase. If you plan to commit to that budget level, we can certainly start working on setting up the new campaigns. Otherwise, I would recommend focusing exclusively on organic reach for the moment.”

Also from the same call: renaming a campaign or ad group resets the learning phase at both levels. And app-install conversion tracking requires a Mobile Measurement Partner (Adjust, AppsFlyer, Branch) — a pixel tracks web events only and cannot see what happens after the store link.

Worth recording that Reddit’s own representative advised against buying Reddit ads at our budget. That is unusually straight advice from someone whose job is selling ad space, and should carry weight accordingly.


Distinct from the app’s UMP work. These are the websites’ consent gates, which had never been tested.

With all Complianz cookies cleared, secondaire.verbomatique.be requested connect.facebook.net, fired facebook.com/tr, defined fbq, and showed zero Complianz placeholders. The banner was displayed but blocked nothing. This had been true since the pixel was installed.

Two independent causes — fixing one does nothing:

  1. Complianz → Integraties → Diensten: the Facebook toggle was off. That toggle is what tells Complianz to gate a service.
  2. The pixel was hardcoded in WPCode as a plain <script>. Complianz can only block scripts it can rewrite. Enabling the toggle alone was tested and changed nothing.

The fix: enable the toggle AND rewrite the pixel as <script type="text/plain" data-service="facebook" data-category="marketing">. Verified both directions — blocked pre-consent, activates on Accepteren, placeholder consumed.

verbomatique.com was already correct

Pixel already blocked pre-consent as a facebook/marketing placeholder. No change made.

GA fires pre-consent there deliberately. Complianz is set to “Yes, with Google Analytics” with all three conditions ticked (Google data processing amendment, no other Google services, IP anonymisation) — Complianz’s documented consent-less analytics setup. Defensible under PIPEDA, which permits implied consent for non-sensitive uses within reasonable expectations; the OPC’s stricter line targets behavioural advertising, which here is opt-in.

Not yet reviewed: GA on the nl-BE site. It fires pre-consent there too, and nobody has checked whether the equivalent statistics configuration is set. The GDPR bar is higher than PIPEDA’s.

PlayStoreClick custom event

Existed on verbomatique.com, missing entirely on the nl-BE site despite 8 store links. Added 19 Aug via WPCode footer: delegated click listener, closest() on an anchor, matching apps.apple.com and play.google.com, plus a locale: 'nl-BE' parameter — both sites share one pixel, so without it the markets blur together. Worth adding the equivalent locale param on verbomatique.com.

Left as a plain script deliberately: it guards on typeof fbq and is inert without consent.

OPC guidance on children (Canada)

The Office of the Privacy Commissioner states it is difficult to ensure meaningful consent from children to online behavioural advertising, and that organizations should avoid tracking children and tracking on websites aimed at children. Both sites address parents, which is already the stated position — but this is now a documented regulator expectation on the Canadian side too, mirroring UCPD Annex I point 28 in Europe. Same fork as the in-app nudge.

Expect nl-BE numbers to drop now that declining visitors send nothing. That is the fix working, not a performance change — do not read the gap against Canada as campaign effectiveness.

Lesson: a consent banner being present proves nothing about whether anything is blocked. Test with cookies cleared and read the network requests.


Discovered while adding the Meta domain-verification tag. This is a separate WordPress install on Combell from secondaire.verbomatique.be, with its own plugin set and its own login.

Before: no Complianz, no banner, no Consent Mode, no cookie policy, no privacy statement. GA4 (_ga_E5G7YHYQ2R) fired on page load. Verified empirically — gtag defined, googletagmanager requested, zero consent commands in dataLayer, zero banner elements in the DOM. Plugin set was Akismet, Colibri Page Builder, Contact Form 7, Contact Form Submissions, Elementor, Limit Login Attempts Reloaded, Site Kit, WPT Demo Importer.

Fixed 22 Aug: Complianz installed and run through the wizard (EU/GDPR region, Belgium-targeting answered Ja, cookie policy generated, statistics via Google Analytics, DNT/GPC enabled, Google Fonts declared, banner and script blocker on); Site Kit Toestemmingsmodus enabled; WP Consent API installed via Site Kit’s own installer.

The deliberate jurisdictional split, worth recording because it looks like an inconsistency: the three consent-less conditions are unticked on verbomatique.be so GA requires consent (GDPR), and ticked on verbomatique.com so GA runs without consent (PIPEDA).

Verified both directions on .be: pre-consent the banner shows and Consent Mode DEFAULT fires with analytics_storage and ad_storage denied; after Accepteren the UPDATE fires granting both, cmplz_statistics=allow, cmplz_marketing=allow, _ga then set.

Honest limits, still open: no privacy statement exists on this site (Complianz free cannot generate one, so it was set to “Geen”); Google Fonts still loads pre-consent — declared but the blocker produced no script placeholders; the script blocker produces no placeholders at all (gtag loads and Consent Mode restricts it, same as Secondaire — acceptable under Consent Mode v2 but not full blocking); the cookie policy page is not in any menu.

Cookie-observation trap: _ga_WNNYZ2EVKE and _fbp appear on verbomatique.be but belong to secondaire.verbomatique.be — cookies set on .verbomatique.be are shared across subdomains. This site has no Meta pixel of its own.

Source: Asana 1217744003003412 (created 22 Aug 2026 09:28)

Read from window.dataLayer on the live site. Default command fires first with ad_storage, ad_user_data, ad_personalization, analytics_storage, functionality_storage, security_storage and personalization_storage all denied, scoped to an EEA region list including BE, with wait_for_update: 500. Update command on accept grants all seven, matching cmplz_statistics=allow. So the chain Complianz → WP Consent API → Site Kit Consent Mode → gtag works end to end. Banner offers Accepteren / Weigeren / Bekijk voorkeuren.

Source: Asana 1217714544140891, comment of 21 Aug 2026 12:07

GA4 & Tracking

verbomatique.com property

  • Account: 198335918 (“verbomatique”)
  • Property: 548294070 (“Verbomatique.com”)
  • Stream: 15374194366 (web, verbomatique.com)
  • Measurement ID: G-7H79V4X8Y7 · Google Tag: GT-MBNVS8CR
  • Enhanced Measurement: ON (Pageviews, Scrolls, Outbound clicks, Form interactions)
  • Search Console: linked (Aug 7, 2026) · Google Ads: linked (Aug 15, 2026)
  • “Exclude All logged-in users” is ON — testing tracking requires temporarily disabling this in Site Kit → Settings → Analytics → Edit, then re-enabling afterward.

Engagement time is not a quality signal on this site — read this first

Do not rank channels by average engagement time, and do not use the “Meta 1s vs Reddit 18s” comparison at all. GA4 stops counting when the page loses focus, and the primary conversion here is an outbound click to the App Store or Google Play. A visitor who lands, sees the badges and taps through converts and ends the session — recorded as a few seconds. A fast conversion and a bounce are indistinguishable by that metric. This comparison was misread for weeks, in both directions: Meta’s short times may mean it converts, Reddit’s long times may mean it reads without converting. Reddit additionally has no conversion pixel, so its conversions are invisible either way.

Report key events instead. The only key events carrying data are click (outbound store badges) and form_submit (newsletter); both starred on property 548294070. purchase, qualify_lead and close_convert_lead are empty — ignore them.

Key events by channel, verified 18–24 Aug 2026:

Channel Sessions Key events Rate
google / cpc 61 10 9.84%
facebook / paid_social 133 3 2.26%

Note this reverses the reading the 19 Aug baseline invites: Paid Search shows as the dead channel there (10 sessions, 0 engaged, 0s) and is in fact converting at more than four times the rate of Paid Social.

The newsletter form is not a conversion path

form_submit fired twice in 28 days, from a single user. Outbound store-badge clicks fired 92 times in the same window. Treat the form as a mailing-list capture, not a conversion metric, and do not re-plan the page around it — moving it higher up the page is not a fix for a 2-in-28-days event. The download proxy (click) is the metric that carries signal. See also the Google Ads note on Aanmeldingen in the Advertising section.

Event names (actual, not placeholder)

  • click — Enhanced Measurement outbound click (fires on store badge clicks)
  • form_submit — Enhanced Measurement form submission
  • page_view, first_visit, session_start, scroll, user_engagement, form_start — standard auto-events

The property and tag inventory (audited 21 Aug, corrected 21 Aug, re-read 24 Aug 2026)

en-CA (verbomatique.com): GA4 property 548294070.

nl-BE (secondaire.verbomatique.be): account 226199012 (verbomatique_secondaire) holds two properties:

  • 311442599 — the live one. Measurement ID G-WNNYZ2EVKE, data stream 3453917046, connected via Site Kit, Enhanced Measurement enabled, plugin conversion tracking disabled.
  • 375937063 — measurement ID G-E298W2THX6, has never received data. Dead duplicate, to be deleted.

Also loading on the site: GTM container GTM-K22HFHPJ alongside gtag.js (Site Kit has both Analytics and Tag Manager connected), and a second measurement ID G-EQDN3BWDSD that is still unaccounted for — probably inside the GTM container or a leftover hardcoded snippet.

Both sites have “Excluded from Analytics: all logged-in users” switched on. This is a standing source of false “tracking isn’t working” conclusions — you cannot test collection while logged in.

Source: Asana 1217714544140891 (created 21 Aug 2026 11:53) and its comments of 21 Aug 12:00 and 12:07

nl-BE GA4 is no longer non-functional — and click is one toggle from being countable

Property 311442599, read 24 Aug 2026. Last 28 days: 50 active users (+138,1%), 280 events (+95,8%), 0 key events. Last 7 days: Belgium 28 users; Paid Search 27 sessions, Paid Social 14, Direct 4, Organic Social 4, Referral 1.

Events actually arriving on stream “verbomatique secondaire”: click, first_visit, page_view, scroll, session_start, user_engagement.

The three configured events — page_view_apple, page_view_google, purchase — all read “Geen streamgegevens gevonden” over 28 days. Two of them are starred as key events, which is exactly why the key-event count reads 0 despite live traffic. They are GA4 “create event” rules that have never matched anything.

click — the outbound store-badge click, the same event the whole en-CA conversion picture rests on — is being collected and is NOT marked as a key event.

Still absent from this property: any form_submit, and any PlayStoreClick (the event added to the site on 19 Aug does not appear under that name). Unverified: whether the locale: 'nl-BE' parameter is present, and per-event counts — the property has no report configured.

Source: Asana 1217773872320434 (created 24 Aug 2026 09:31)

en-CA event volumes behind the form_submit finding (read 24 Aug 2026)

GA4 property 548294070, event counts by name:

  • 18–24 Aug: click 14 events / 10 users; form_start 1; form_submit does not appear at all.
  • 27 Jul – 23 Aug (28 days): click 92 events / 45 users; form_start 5; form_submit 2 events from one single user — almost certainly a test.

The site converts at roughly 7% (294 sessions, 20 key events over 7 days), carried entirely by two starred key events: click and form_submit.

Sources: Asana 1217776430661860 (created 24 Aug 2026 08:01); Asana 1217714544140891, comment of 23 Aug 2026 19:32

Stale starred key events on verbomatique.com

close_convert_lead, purchase and qualify_lead are starred as key events but show no stream data — GA4 e-commerce defaults that have never fired. Harmless, but they clutter every conversion report.

Source: Asana 1217796351575595 (created 24 Aug 2026 15:36)

App Store (Canada): https://apps.apple.com/ca/app/verbomatique-secondaire/id1611881585 App Store (Belgium): https://apps.apple.com/be/app/verbomatique-secondaire/id1611881585 Google Play (global): https://play.google.com/store/apps/details?id=be.teamwabisabi.verbomatique_secondaire

Play Store badges on verbomatique.com carry a referrer tag: utm_source=google_play_badge&utm_medium=referral&utm_campaign=back_to_school_2026 (updated Aug 15, 2026).


Primaire’s privacy policy is live on GitHub Pages (22 Aug 2026)

https://teamwabisabi.github.io/verbomatique/privacy/nl-be — verified serving real content, HTTP 200. Committed as 56b6aa5.

Structural fact worth recording: there is no standalone teamwabisabi.github.io repo. Both privacy policies are GitHub Pages project sites on the respective app repos — source main branch, /docs folder, Jekyll minima theme. Pages was not enabled on the verbomatique repo before 22 Aug; it is now (main/docs, legacy build).

Primaire’s is nl-BE only (Dutch-only app). Secondaire’s, at https://teamwabisabi.github.io/verbomatique_secondaire/privacy/nl-be, has locale-aware paths including en-ca and was last updated 17 July 2026.

Deliberately not copied from Secondaire into Primaire’s: app-ads.txt (declares an ads-seller relationship, wrong for a no-ads app), and the AdMob, Crashlytics, newsletter and in-app-purchase sections.

After the rebrand these paths will say verbomatique_secondaire and verbomatique for what are by then Verbomatique and Verbomatique Junior. Store listings point at these URLs, so changing them is not free — either accept the mismatch (as with the app IDs) or plan redirects.

Source: Asana 1217744288172445, comment of 22 Aug 2026 15:49

Technical Patterns That Work

Editing the two websites

Moved to its own section — see Website Editing, which now holds the REST route, the code-editor route, and the Colibri techniques in one place.

Brevo form editing

The form HTML lives in the sibformmarkup textarea on the Brevo plugin’s form edit page. Use the same native setter JS pattern to replace text, then click Save. The form preview updates on save, not on edit.

When adding ads in the wizard, you MUST click the “Klaar” (Done) button at the bottom of the ad editor to commit the ad. Without it, the Review summary shows “Advertenties: Geen” and the publish button doesn’t appear — this is not a display bug, it’s the ad being in uncommitted editing state.

Meta Ads Manager “Invalid request (#1)” — it is a LOCAL fault, not Meta

Symptom: campaigns table empty, “Kan concept niet opslaan: Invalid request. (#1). Je kunt in dit account geen advertenties aanpassen”, reporting unavailable. Two days were spent treating this as a Meta-side outage and preparing an escalation.

It is client-side. The fix has two parts and both matter: clear cookies for facebook.com and instagram.com, then enter Ads Manager via the base URL (adsmanager.facebook.com), not a bookmarked or history-completed deep link carrying saved view state. Clearing cookies alone may not be enough.

Diagnostic that isolates it in 30 seconds: open the account in a private window. If it works there, it is local.

General lesson: when a platform surface fails but the platform’s own account-health checks are clean, test in a private window before assuming the platform is broken.

Meta uses the File System Access API for uploads

The Uploaden button calls showOpenFilePicker(), not an <input type=file>. There is no file input in the DOM to write to and the native dialog is outside the page, so file uploads to Meta cannot be automated by browser tooling. Everything either side of the dialog can be.

Complianz blocks only scripts it can rewrite

See the Website Consent section. A hardcoded WPCode script is invisible to Complianz regardless of the service toggle. Requires type="text/plain" plus data-service / data-category.

WPCode’s editor auto-closes script tags

Typing a full <script>…</script> produces a duplicated closing tag and a syntax error. Type the opening tag and body only; the editor supplies the close.

Complianz site scan location

Not under Tools. It is Wizard → Toestemming → Sitescan.

Brevo form styling — the Form Style panel, and the quote trap

The Brevo plugin’s form edit page has two editable areas, and it is easy to see only the first:

  • #sibformmarkup (sib_form_html) — the field markup
  • #sibcssmarkup (sib_form_css) — the Form Style panel, further down the page, radio set to “Custom style”

[form] in that CSS is a placeholder, substituted by the plugin with #sib_signup_form_2 at render time. It is not a broken attribute selector. Do not “fix” it.

The plugin HTML-encodes double quotes on save. input[type="text"] ships to the front end as input[type=&quot;text&quot;], which is an invalid selector, so the browser silently drops the entire rule. Rules containing no quotes survive, which produces the confusing symptom of some elements being styled and others not. Write the whole stylesheet without quotes — unquoted attribute values and unquoted font names are valid CSS. A comment at the top of the current stylesheet says so; keep it there.

Diagnosing: don’t read the CSS textarea and conclude. Load the front end and check getComputedStyle on an actual input, and grep the emitted <style> block for &quot;.

en-CA form was wearing the nl-BE stylesheet (fixed 24 Aug 2026)

en_CA_LandingPageForm (id=2) carried CSS written for the Dutch form: selectors referenced sib-AUDIENCE_NL-area and sib-CONSENT_NL_BE-area. The en-CA form uses AUDIENCE and CONSENT_MARKETING, so every rule for the radio group and the consent row matched nothing. Only locale-neutral rules applied — which is why the “Email address” label was styled and “First name” was not.

Rewritten 24 Aug with the correct attribute names. The markup was also fixed: the AUDIENCE block had <div> inside <p> (invalid — browsers auto-close the <p>), now <span class="vb-radio">; and a stray <label>*</label> before the consent checkbox was removed.

This affects the homepage too — same shortcode [sibwp_form id=2]. The live homepage form had been unstyled since it was installed.

Not checked: whether the nl-BE form has the mirror-image problem.

Working with Claude Code on this repo

Observed consistently across the 18–19 Aug consent work: work that produces code gets done; work that produces evidence gets dropped from the summary. Test runs, greps and file dumps were repeatedly requested, performed, and then omitted from the report. Treat unreported items as not done. Ask for raw output (! shell escape) rather than a summary, and read the numbers yourself — on two occasions the summary’s conclusion contradicted the data directly above it.


Test behaviour, not markup

Two confident false negatives in this project came from the same mistake — reading page source instead of observing behaviour.

On 21 Aug the finding “there is no gtag('consent','update') call” came from regexing inline script text. Complianz fires that call dynamically from its own JS file, so it could never appear in inline markup: the detection method was incapable of finding the thing it concluded was missing. Reading window.dataLayer on the live site showed Consent Mode v2 fully working.

The 22 Aug typographic-apostrophe false negative is the same shape — and note the inverse trap: template source uses ASCII apostrophes while the rendered page uses typographic ones.

The rule: before reporting something absent, satisfy yourself the check could have detected it if it were present. Read dataLayer, read network requests, read getComputedStyle.

Sources: Asana 1217714544140891, comment of 21 Aug 2026 12:07; Asana 1217790212813686 (24 Aug 2026)

A GA4 property ID and a measurement ID are different identifiers for the same property

Property 311442599 is G-WNNYZ2EVKE. Comparing a property number against a measurement ID proves nothing — a whole “data is going to an unknown property” finding on 21 Aug came from exactly that, and was wrong.

Source: Asana 1217714544140891, comment of 21 Aug 2026 12:00

Instruct Claude Code to paste raw output and flag ambiguity rather than resolve it

This is what surfaced the Firebase SDK in the Primaire Android binary on 22 Aug. A summary would have said “no ads, no analytics” — true of pubspec, false of the shipped binary. It also produced an unprompted correction of the auditor’s own earlier AD_ID claim.

Source: Asana 1217744288172445, comments of 22 Aug 2026 09:59 and 10:41

Which is why form values in a browser session should be set programmatically rather than typed.

Source: Asana 1217790212813686 (24 Aug 2026), recording the contents of project-knowledge-website-editing.md

The WordPress.com MCP connector does not include DNS

The raised scopes of 24 Aug cover content, not DNS. wpcom-domain-update-dns-records returns “The ‘wpcom/domain-update-dns-records’ ability is not enabled in your MCP settings”, as do wpcom-user-sites and the DNS read abilities. The DNS records on 25 Aug were added through the WordPress.com dashboard at wordpress.com/domains/manage/verbomatique.com/dns instead.

Enable at wordpress.com/me/mcp if a future session should do DNS directly. Until then, plan DNS work as browser work.

Related trap: wpcom-ai-agent-sites-list is enabled but is a public discovery tool — it returns unrelated WordPress.com blogs, not this account’s sites. Do not read its output as “the user’s sites”.

Source: tool errors observed 25 Aug 2026.

A reply sent by API is not a test of Reply-To

The Gmail MCP reply tool addresses the message’s From, not its Reply-To. Using it to test a reply path produces a bounce that looks like a broken Reply-To and is not one.

To test Reply-To, reply from the actual mail client and read the To field it pre-fills. On 25 Aug the API reply bounced off hello@news.verbomatique.com while the Gmail web client, on the same message, correctly pre-filled decibellen@gmail.com and delivered.

The bounce was still worth having — it is what surfaced the missing MX. But it would have been reported as a Reply-To failure by anyone who stopped at the first result.

Source: Asana 1217778600752666 and 1217814220575319, both 25 Aug 2026.

DKIM CNAME chases time out from the cloud container — check a known-good control first

Resolving brevo1._domainkey.<domain> for its TXT (chasing the CNAME into dkim.brevo.com) times out from the cloud container against 8.8.8.8, 1.1.1.1 and 9.9.9.9 alike, while the CNAME itself and the target’s TXT both resolve fine when queried separately.

This is an artefact of the container’s resolver path, not a broken record. Confirmed by running the same query against brevo1._domainkey.verbomatique.be, a domain Brevo already reports as Authenticated — it times out identically.

Before reporting a DKIM record as unresolvable, run the same query against a domain known to work. Otherwise the check cannot distinguish “record missing” from “resolver path broken”.

Source: DNS queries run 25 Aug 2026.

Check marketing claims against the app’s actual verb and tense inventory before publishing them

On 25 Aug a set of conjugation marketing cards was built covering twelve verbs across présent, imparfait, futur simple and passé composé. Reading the repo showed devoir is not in the app (no verb_devoir.dart), passé composé is not a tense it drills, and conditionnel présent, which it does drill, was absent from the cards. Fifteen of forty-eight cards taught content the app could not deliver.

The error’s origin is worth recording: the verb list was taken from the marketing artefact itself and then asserted back as the app’s inventory. Nothing checked the two against each other because both came from the same place.

A file count is not a shipped inventory — even when it turns out to match. A Claude Code session reported thirty-two files matching verb_*.dart in lib/repository/verbs/. Within one message that became “the app ships thirty-two verbs” and then a planning conclusion about content runway. The number was later confirmed correct by a direct check — but nothing in the chain that produced it justified asserting it, and it would have been repeated with equal confidence if it had been wrong. A glob cannot distinguish the basic set from gated content, test fixtures, or unreachable code; it happened to here because no gated verb content exists yet.

Carry the scope with the number, always. Thirty-two and forty-three are both correct counts of verb_*.dart — scoped to lib/repository/verbs/ and recursive across lib/ respectively — and a figure quoted without its command invites a reader to treat one as refuting the other. The eleven-file difference is not gated or unshipped verb content: it is routes, models and widgets whose names merely begin with verb_ (verb_card.dart, verb_metadata.dart, verb_indicative_test_route.dart and eight more). The recursive count is not a verb count at all, which is the sharper reason never to quote it as one.

The number that actually means something is the registration list. FrenchRepository._internal() instantiates each verb explicitly, so its length is the shipped inventory by construction, and a verb file that exists but is not registered would not ship. Re-derive with ls lib/repository/verbs/verb_*.dart | wc -l and by counting the entries in that constructor — both read 32, re-verified 26 Aug 2026 at commit f2ff92f.

The pipeline’s --verb all is eleven invented verbs, not the app’s set

ALL_VERBS in scripts/screenshots.sh holds eleven verbs. They are not a subset anyone chose from the product. They are the twelve most common irregular French verbs, picked from general French knowledge by a Cowork session on 25 Aug 2026 for a marketing card deck, before any repo had been read — minus devoir, dropped when Claude Code found no verb_devoir.dart.

That list was then written into the Claude Code brief as though it were the app’s inventory, hardcoded into the pipeline, and read back out as “the marketing eleven” — a guess laundered through two artefacts and returned as fact. The same brief told Claude Code to verify the tense axis and not the verb axis, for no reason other than that the author happened to feel uncertain about one and not the other.

Fixed 25 Aug 2026. --verb all now derives the set from the app’s own registration via scripts/list_verbs.py, so it cannot drift from the picker; the invented eleven moved to --verb marketing with a comment recording what they are; and every run prints which set it resolved and how many. The lesson survives the fix: a short hardcoded list inside a script is indistinguishable from a derived one at a glance, which is how this travelled four artefacts without being questioned.

Source: Cowork session of 25 Aug 2026, tracing its own artefacts

ls lib/**/verb_*.dart | wc -l returns 43, not 32. Do not count verbs this way. The recursive glob sweeps in eleven files that are not verbs: verb_metadata.dart, verb_test_model.dart, verb_repository.dart, six verb_indicative_* route files, verb_selectionmode_selection_route.dart and verb_card.dart. Scoped to lib/repository/verbs/ it gives 32; so does the registration list. This is not a hypothetical trap — that exact command was written into a brief on 25 Aug as the way to settle the question, and it would have produced a fourth different number in a conversation already juggling three.

The rule: any marketing asset naming a specific verb, tense or mode is a claim about the product. Re-derive it from the repo rather than from another marketing document — and derive the right thing. A glob answers “what files exist”; the question a marketing card raises is what a user can open today, and only the registration-to-wheel chain answers that.

Note that the tense set is not uniform: futur proche and passé récent exist only on aller. A sweep that assumes every verb defines every tense will silently produce nothing for most of them.

Source: Claude Code session of 25 Aug 2026, repo read; marketing deck corrected the same day

The git operating notes are in the merge-protocol doc, not CLAUDE.md

The lock-file enumeration, the fact that git status is a write, the no-push constraint and the commit identity all live in the project doc claude/knowledge-file-merge-protocol.md under “Operating notes for Claude sessions on this repo”. They are not in the repo’s CLAUDE.md on any branch — verified 25 Aug 2026 when a brief pointed there and Claude Code found nothing on either main or the harness branch.

Worth fixing at the source: these are operating notes for anyone working the repo, and they are filed inside a document about merging a knowledge file, which is not where anyone looks for them.

Source: Claude Code session of 25 Aug 2026, having read both branches’ CLAUDE.md

There are two screenshot harnesses, on different platforms, for different locales

Easy to miss because the file describes screenshot work in two places without connecting them.

  • iOS, en-CAharness/en-ca-screenshots: iPad mini A17 Pro simulator by UDID, locale via simctl + reboot, capture via xcrun simctl io … screenshot, bezelled by add_bezel.py ipad_mini. Native 1488×2266, bezelled 1732×2510.
  • Android, nl-BE — an untracked integration_test/marketing_nl_be_test.dart on main plus a marketing_screenshots_nl_be/ folder. Native 1080×2424. This is the harness behind the nl-BE back-to-school creative.

Both were superseded on 25 Aug by a single parameterised pipeline (scripts/screenshots.sh + integration_test/screenshot_pipeline_test.dart) driving either platform. The 1080×2424 figure quoted elsewhere in this file is the Android frame; the iOS branch harness never produced it.

Source: Claude Code session of 25 Aug 2026, reading both harnesses

ATT can be suppressed on the iOS simulator without touching the ConsentApi seam

Settings → Privacy & Security → Tracking → Allow Apps to Request to Track, switched off, makes the request resolve denied without drawing a prompt. This is the clean way to take iOS screenshots despite _requestATT sitting outside the test seam — it does not require weakening the seam.

Consistent with Apple developer-forum reports; not yet confirmed on this project’s simulator. Confirm before relying on it.

Separately, on the branch the pipeline was built from, the Dart-side app_tracking_transparency channel mock does suppress the dialog — ATT never fires there at all, because ConsentManager only exists on consent/ump-flow. The 24 Aug incident where a real dialog appeared is therefore specific to running against that unmerged branch.

Source: Claude Code session of 25 Aug 2026; Apple Developer Forums threads 669490 and 702582

A brief assembled from this file asserted four stale facts in one page

On 25 Aug a Claude Code brief was written by quoting this knowledge file. Four claims were wrong by the time it ran, and Claude Code caught all four only because the brief told it to verify before acting:

  • the harness branch described as unpushed (it was pushed),
  • consent/ump-flow described as two commits ahead (five),
  • the canServeAds invariant cited as authoritative for a branch that does not contain it,
  • a platform recommendation resting on an ATT failure mode that does not occur on that branch.

The lesson is narrower than “the file goes stale”. Branch state and commit counts are the fastest-decaying facts in here and should be re-derived at use, never quoted. Anything scoped to an unmerged branch must say so at the point of citation, because a reader will otherwise apply it to whatever branch they are on. Instructing the agent to verify is not a substitute for not asserting.

Source: Claude Code session of 25 Aug 2026, reported verbatim

Brevo’s HTML editor: the working URL, and two different save semantics

The editor URL is https://app.brevo.com/editor/classic/html/{id}. The path https://app.brevo.com/templates/editCode?id={id} 404s — it is a plausible-looking guess that does not exist. Reach a template’s editor from the Templates listing pencil icon (/templates/email/edit/{id}) if the direct URL is ever in doubt. An automation-step message takes query parameters: .../editor/classic/html/{id}?automation_id={a}&step_id={s}.

Saving works differently for the two object types, and neither is trustworthy from the UI:

  • Template. “Save & Quit” in the HTML editor only stages the change. The commit is the Save item on the template page that follows. Skipping the second step loses the edit silently.
  • Automation-step message. “Use this design in automation” persists on its own — no second step. But the click silently no-ops often, with no error and no visual difference between the failed click and the successful one. On messages 10, 12 and 13 it took two clicks, and once it took three.

“Save & Quit” on templates also no-oped at least once each on templates 7, 8 and 9.

The only reliable confirmation is reading the object back from the API with templates_get_smtp_template and grepping the returned htmlContent for the new string. Roughly thirty edits were made across eight objects on 26 Aug; every one was verified that way, and the verification caught a silent failure several times. Do not trust a green toast.

Source: ~30 edits across templates 6–9 and automation messages 10–13, each verified from the API, 26 Aug 2026

Editing Brevo’s HTML editor by script, not by typing

Typing into the editor’s <textarea> does not reliably register — React does not see the change. The pattern that works, used for every edit on 26 Aug, sets the value through the native setter and dispatches the events React listens for:

const ta = document.querySelector('textarea');
const o = '<old string>';
const n = '<new string>';
let v = ta.value;
const c = v.split(o).length - 1;
if (c !== 1) throw new Error('count ' + c);   // abort unless the anchor is unique
v = v.replace(o, n);
const s = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
ta.focus(); s.call(ta, v);
ta.dispatchEvent(new Event('input',  {bubbles: true}));
ta.dispatchEvent(new Event('change', {bubbles: true}));
ta.blur();
ta.value.includes(n);   // returns true if it took

The uniqueness assertion is the part that matters. The welcome emails repeat phrases between paragraphs, and a replace on a non-unique anchor silently edits the wrong one. Assert the count is exactly 1 and throw otherwise — the same discipline the knowledge-file merge protocol requires, for the same reason.

For a structural change rather than a string swap — the 26 Aug pass moved the product-explanation paragraph above the free-app paragraph in all eight emails — build the new value by slicing on asserted markers and check the result’s ordering before dispatching, rather than writing a long literal.

Source: the 26 Aug 2026 editing pass across all eight Brevo email objects

browser_batch races the Brevo UI

Batching clicks against Brevo lands them on stale coordinates: a campaign name typed into a page that had already navigated, a subject field missed because the panel had scrolled underneath. Interleave explicit wait actions and screenshot at every decision point. For the irreversible steps — anything that sends — drop to one action per call and read the result before the next.

Source: repeated failures and recoveries while building campaigns 14–17, 26 Aug 2026

Key Files in This Project

| File | What it is | |—|—| | CLAUDE.md (in the app repo, not this project) | Code invariants, architecture, testing conventions, and the working agreement for Claude Code. Read automatically every CC session. | | verbomatique-secondaire-roadmap.md | Living product roadmap (monetization tiers, feature status) | | en-CA-back-to-school-launch-plan.md | Complete campaign plan with budget, timeline, tasks, A/B test spec | | claude-design-brief-meta-ads.md | Brief for producing Meta ad creative variants | | verbomatique-pricing-signup-copy-update.md | Copy changes for both sites (mostly applied) | | verbomatique_secondaire_ad_preview.png | Original judo club placemat ad (source of brand color extraction) | | Verbomatique_EN_Canada_Campaign_Plan_FINAL.docx | Original en-CA campaign plan document | | verbomatique-inline-signup-form.html | Standalone nl-BE signup form markup | | harness/en-ca-screenshots (git branch) | Screenshot harness split off main 24 Aug. Committed and pushedorigin/harness/en-ca-screenshots exists, verified 25 Aug 2026. iOS/iPad-mini only. See scripts/README-screenshots.md on that branch. Superseded 25 Aug by the parameterised pipeline on harness/screenshot-pipeline. | | blaze-en-CA-landing-page-and-ads.md | Blaze page copy, ad variants, targeting config | | blaze-creatives-revision-2.md | Creative revision brief incl. the background decision | | claude-design-brief-blaze-creatives.md | Original Claude Design brief | | verbomatique-com-wordpress-build-guide.md | How the en-CA site was built: theme, fonts, Brevo setup, block-by-block page construction |


File What it is
project-knowledge-website-editing.md Rewritten 24 Aug. Supersedes the 23 Aug “REST API does not work” note. Three edit routes, the iotix//home fingerprint, the verification protocol, rollback recipes. Sits in the outputs folder — not yet pasted into project knowledge.
google-search-en-CA-keyword-rebuild-2026-08-24.md Full keyword and negative lists behind the Phase 1/Phase 2 Google Search rebuild.
claude/tfua-review-log.md (project doc) Append-only log for the monthly AdMob Belgium / TFUA review.

Source: Asana 1217790212813686 and 1217582438101941 (both 24 Aug 2026); project doc claude/tfua-review-log.md

File What it is
claude/verbomatique-social-media-plan.md Organic social plan for Instagram + Facebook, 25 Aug 2026. Six-week calendar, seed posts, measurement, kill criterion, and the two legal constraints on any post. Artifact version at claude.ai/code/artifact/2b55ae33-beb4-49d1-afb0-e0c2fc5f719c.
claude/claude-code-screenshot-pipeline-prompt.md The brief that produced the screenshot pipeline. Retains the tool-choice reasoning (why Claude Code and not a design tool) and the platform-per-mode rule.
scripts/screenshots.sh (repo, harness/screenshot-pipeline) Parameterised marketing-screenshot pipeline. Flags for platform, locale, verb, tense, mode, screen, score. Both iOS and Android. Output gitignored.
Card studio (artifact) Generates the five product cards + captions at 1080×1350, both markets, app icon embedded. claude.ai/code/artifact/f7317cc1-f7e7-4cdd-a0c2-4081ff3c0c42

Source: Cowork session of 25 Aug 2026

What’s Still Open (as of Aug 24, 2026)

Each task: owner · effort · when · done when. “Dries” = manual, needs a human in the loop. “Claude” = can be done in a Claude in Chrome session.

  • C1 · English ATT prompt string — built and delivered to ASC 18 Aug, awaiting Apple approval. iOS-only (ATT is an Apple framework). Done when: live in the App Store. Matters more than its size: Canada iOS is the best-earning segment (€6.41 eCPM) and those users were seeing an unreadable Dutch prompt.
  • C2 · Interim TFUA policy for nl-BE — decided: blanket true. Belgian revenue ~€3.50/yr makes conditional logic not worth building. Review trigger at €10/month.
  • C3 · UMP consent flow — built and committed on consent/ump-flow. Not merged, not released. Outstanding: wrap _requestATT in try-catch.
  • C4 · Gate advertising ID on consent — absorbed into C3; both calls were dead code and were deleted.
  • C5 · Set RequestConfiguration — one-line change, tagForUnderAgeOfConsent: true at the gatherConsent call site. Leave tagForChildDirectedTreatment UNSPECIFIED (app is not child-directed under COPPA; no under-13 audience declared).
  • C6 · Device QA matrix — the real release gate. All 208 tests run against fakes; nothing has touched a live UMP form or a real ATT prompt. Cover nl-BE grant/deny/revoke and en-CA on both platforms, plus cold-start ordering.

Urgency note: exposure scales with marketing success. Today a consent gap touches a handful of Belgian sessions a week; if nl-BE back-to-school lands, it touches far more people in the EEA during the month traffic is actively being driven at it.

In-app upgrade nudge (Asana subtasks)

  • Decide: accept “no in-app exhortation” for lanes B and C?
  • Decide: build age-signal integration now, or ship Lane C-only first? — C3 provides a lifecycle hook where routing can attach, but not the age signal itself. Consent status says nothing about age; that needs the Apple/Play APIs separately.
  • Decide: is a staged-rollout A/B acceptable as the measurement? — no backend, so store-layer A/B avoids adding an analytics SDK (and its consent, privacy-policy and minors-data implications).

Campaign & data hygiene (carried over)

  • Weekly: export nl-BE leads and import to Brevo — superseded 25–26 Aug 2026. The nl-BE Pabbly workflow is live and proven delivering, so new leads reach list 2 automatically, and the 20 leads stranded before it existed were imported the same day. The stopgap cloud task that bridged the gap was deleted 26 Aug. Nothing recurring remains here — but note that nothing now diffs Meta against Brevo on a schedule either.
  • Backfill the pre-Aug-10 en-CA leads — Dries · 20 min
  • Set LOCALE = en-CA on backfilled contacts during import
  • Add static LOCALE = en-CA to the en-CA Pabbly Brevo action — Claude · 2 min
  • Deactivate the half-built nl-BE Pabbly workflow — moot as of 25 Aug 2026. It was never active and consumed 0 tasks; it has since been completed and activated deliberately, with an action step attached, and shows 2 tasks consumed as of 26 Aug. The underlying warning still stands for any future half-built workflow.
  • Rename Brevo list “Your first list” → “België (Nederlands) Subscribers” — done. Verified 26 Aug 2026: lists_get_lists returns id 2 named “België (Nederlands) Subscribers” (23 contacts) and id 3 “Canada (English) Subscribers” (37 as of 18:03; it read 39 earlier the same day — see “Three contacts sit outside every list”). No list called “Your first list” exists.
  • Welcome email copy check — done 26 Aug 2026. All eight objects (templates 6–9 and automation messages 10–13) were rewritten over five passes and each edit was verified by reading the object back from templates_get_smtp_template. No “paid version upcoming” language survives anywhere: every version now names the shipped one-time upgrade at the correct currency (CAD $1.99 / €1,99). The practice-method names were corrected to the canonical set at the same time.

Unblocking real Lead Ads automation (sequential)

  • 1. Complete Facebook developer registration — done when: developers.facebook.com shows a “Create App” button
  • 2. Create a Meta app and associate it with the Verbomatique secondaire page
  • 3. Generate one test lead on VS nl-BE Back to School - Nieuwsbrief
  • 4. Finish the nl-BE Pabbly action and activate

Back-to-school campaigns — both markets

Flemish schools start Tue 1 Sept; Canadian schools around 7 Sept (Labour Day).

  • nl-BE back-to-school campaignLIVE since 19 Aug, running 20–31 Aug. See the Advertising section.
  • en-CA back-to-school pivot — baseline now frozen (see below), so this is unblocked.

Baseline frozen 19 Aug 2026 (pre-pivot, en-CA)

  • Meta Traffic: 217 landing page views, €0.21/LPV, €46.39 spent
  • Meta Leads: 17 leads, €3.61/lead, €61.39 spent
  • en-CA Meta spend in window: €107.78 (20 Jul – 18 Aug)
  • GA4 28-day: 508 sessions, Paid Social is 84.84% of all sessions.
  • Reddit 6–18 Aug: €85.59, 30,982 impressions, 165 clicks, €2.76 eCPM, €0.52 CPC, 0.533% CTR

This baseline has no key-event figure, which is the metric that matters. The engagement figures originally recorded here (16.14% engagement, 4s avg; Paid Search 0s, Organic Search 11s, Unassigned/Reddit 18s) have been removed as a channel-quality measure — see “Engagement time is not a quality signal” in GA4 & Tracking. There is therefore no clean before-picture on key events; the 18–24 Aug figures are the earliest reliable reading. Note that limitation explicitly rather than substituting engagement time for it. Any re-baselining drawn on engagement will conclude the campaign degraded traffic quality when it may have improved conversion.

Near-term (product)

  • Extra verb-set IAPs (dev work, not yet started)
  • Subscription tier pricing decision
  • Write-mode feedback tick hidden behind keyboard (Android only; iOS keyboard is semi-transparent and shows it)
  • Gender-neutral pronouns as default, with settings toggle

Deliberately deferred

  • Mobile measurement partner (Adjust/AppsFlyer) — not worth building for current budget

The monthly TFUA review runs as a cloud scheduled task

Scheduled task trig_019nKHy9B2n9KLJMQquJwosf — “[CLOUD] Monthly TFUA review — Verbomatique Secondaire (AdMob Belgium)”, cron 0 7 6 * * (6th of each month, 09:00 Brussels), created 18 Aug 2026 13:19, last updated 18 Aug 2026 13:30, enabled, next run 6 Sept 2026. Confirmed present in the scheduled-task registry on 24 Aug 2026.

The [CLOUD] prefix is deliberate: scheduled tasks stored locally by the Cowork desktop app are invisible to Claude sessions and cannot be edited or deleted from one. The prefix is how you tell which is which.

Decision rule: Belgian revenue < €10 / 30 days → blanket TFUA stays; ≥ €10 → build conditional TFUA off the consent signal and reopen Asana task C2; match rate < 85% → note separately as a fill/config problem rather than a volume one.

Open item for the 6 Sept run: if two entries land in the log for that date, the old local Cowork task still exists and must be deleted by Dries in the desktop app. One run means the cleanup was complete.

Source: project doc claude/tfua-review-log.md; scheduled-task registry read 24 Aug 2026

The weekly nl-BE export scheduled run is broken — resolved 25–26 Aug 2026

Something stalled at 09:07 on Monday 24 Aug after listing Brevo lists, and four leads were believed stranded.

Three corrections to the 24 Aug account. The stranded-lead count was not four: the form held 21 leads on 25 Aug (20 real plus one test submission), none of which had reached Brevo. The task as specified could not have completed anyway, because the Brevo connector cannot write contacts and the opt-in attestation needs a person. And “Weekly nl-BE lead export → Brevo import” is not the name of any scheduled task — it is a phrase from the Asana notes that was quoted back as if it named something. What ran at 09:07 remains unidentified; do not assert an answer.

The durable fix landed the same evening: the nl-BE Pabbly workflow was built, tested against live data and activated, and the 20 stranded leads were recovered from Leadscentrum and imported. Verified delivering on 26 Aug — see “The nl-BE workflow is LIVE — and PROVEN DELIVERING as of 26 Aug 2026”. The stopgap cloud task built to bridge the gap was deleted 26 Aug.

Source: Asana 1217775027491967, created 24 Aug 2026 09:31, corrected by the comments of 25 Aug 2026; Pabbly, Meta and Brevo all re-read 26 Aug 2026

Reddit — closed out 26 Aug 2026

Four Asana tasks were closed on 26 Aug when Reddit advertising was discontinued:

  • P7 · Relaunch Reddit for September (1217542445406771) — the flight itself.
  • R1 · Install the Reddit Pixel (1217633136866788) — it was the gate on spend; with no spend it gates nothing. The pixel measures Reddit ad traffic only, so nothing else loses measurement by its absence.
  • R2 · Naming convention (1217633033987496) — no September entities to name.
  • R3 · Creative (1217632914852105) — including the promote-a-proven-organic-post route, since promoting a post is still paid spend.

Still open, deliberately undated: the Reddit listening exercise (1217639836805570). It is unpaid research and is now the only Reddit activity on the board.

The MMP verdict is reinforced, not changed. The only recent push to build one came from Ussama’s 24 Aug follow-up (“the necessity of implementing a Mobile Measurement Partner (MMP) for accurate conversion tracking for app installs”). That request now has no campaign behind it. “Not worth building for current budget” stands, and removing a channel makes the spend smaller.

Source: Asana, tasks closed 26 Aug 2026

The scheduled-task inventory — and a task name that never existed

Dries listed the Cowork desktop scheduled tasks on 25 Aug. Six existed, and the recorded name “Weekly nl-BE lead export → Brevo import” matched none of them:

Task Schedule Where
Weekly ad performance review Mondays ~09:00 local — disabled 26 Aug
[CLOUD] Weekly nl-BE lead export → Brevo handoff Mondays 09:00 cloud — deleted 26 Aug
Weekly nl be ad performance review verbomatique Mondays ~11:00 local
[CLOUD] Monthly TFUA review 6th, 09:00 cloud
Notion knowledge layer review once, 28 Sep local
Verbomatique brevo meta lead check weekly, Paused local

“Weekly nl-BE lead export → Brevo import” is not a task. It is a phrase. It originates in Asana 1217775027491967’s own notes, was copied into this knowledge file, and was then quoted back — by a later session — as if it named something in a registry. Nobody had looked.

Consequences worth keeping: there was never a duplicate export task, so never a double-run risk to clean up; and what actually ran at 09:07 on 24 Aug remains unidentified. Task 1 fits the time and does open Brevo’s contact-list page, but its brief is scoped to en-CA and contains no export step. Task 3 is the nl-BE counterpart but runs at ~11:00. Neither is a clean match and neither should be forced into one.

Verification note, 26 Aug. The two [CLOUD] rows were confirmed independently against the scheduled-task registry: ids, cron expressions and next-run times all match. The four local rows cannot be confirmed from a Claude session at all — the registry returns cloud tasks only. That is not a gap in this table; it is the same invisibility the [CLOUD] prefix exists to signal, and it means any claim about a local task is only ever as good as the human who read it off the screen.

Source: desktop scheduled-task list screenshotted by Dries 25 Aug 2026; cloud registry read 26 Aug 2026

The weekly nl-BE handoff task is deleted — 26 Aug 2026

trig_01YATwUuS4j8LmtKkq34FmTh — “[CLOUD] Weekly nl-BE lead export → Brevo handoff (Mon 09:00 Brussels)”, cron 0 7 * * 1has been deleted. It never ran.

It was created 25 Aug as a stopgap: read the leads out of Meta, diff them against Brevo list 2, hand Dries an import-ready CSV, and stop short of the import the connector cannot perform. Its own step 5 told each run to check whether list 2 had gained contacts it did not hand over, and if so to declare itself redundant.

That condition was met on 26 Aug, four days before its first run: contacts 65 and 66 arrived in list 2 automatically via Pabbly. The task was answering a question that no longer needed asking, so it was removed rather than left to run once and retire itself.

What is lost with it, and does not matter: the CSV handoff. What replaced it is better — Pabbly writes the contact directly, with consent and locale, within minutes of submission. What is lost and does matter slightly: nobody now diffs Meta against Brevo on a schedule, so a silent Pabbly failure would go unnoticed until someone looked. The free-tier ceiling (100 tasks/month) is the most likely cause of such a failure. If that becomes a worry, the check to rebuild is a count comparison, not a CSV pipeline.

Source: scheduled-task registry, deleted 26 Aug 2026 at Dries’ instruction

The en-CA weekly ad-performance brief is carrying superseded facts

The prompt of the local “Weekly ad performance review” task was read in full on 25 Aug. It is a detailed and careful brief, and several of its stated facts have since been corrected elsewhere in this project — so every Monday run re-injects them:

  • Reddit. It attributes €85.59 over 13 days to the relaunch campaign and says ~€13 remains. Both are the error this file already records. It is now doubly stale: Reddit advertising was discontinued on 26 Aug 2026, so the brief instructs a weekly review of a dead channel.
  • Brevo list #3 completeness. It gives a 22 Aug baseline of “21 contacts, only 3 carry CONSENT_MARKETING and AUDIENCE (ids 3, 16, 27)”. Id 3 was moved to list 2 by the locale migration, so the true figure was 2 of 20. List 3 holds 37 as of 26 Aug 2026 18:03, and every one of them now carries AUDIENCE.
  • Uitgaande klik. It instructs the run not to flag this as misconfigured. That contradicts an open priority task to make it the primary conversion. A standing “do not flag” is the most dangerous kind of stale instruction: it suppresses a real finding silently, every week.
  • Google Ads figures. The 22 Aug snapshot (2,441 impressions, 1.69% CTR) invites a false declining-CTR reading, since essentially all lifetime delivery happened in one week.
  • Meta Leads Center. It says the terms screen may be unset up and instructs the run not to accept it. Accepted 25 Aug and confirmed gone 26 Aug; the fallback is no longer needed.
  • Its Asana anchor. It tells each run to find a recurring task called “Weekly channel performance review and reallocation” — which another open Asana task states does not exist.

The general shape, which is the durable lesson: a long-lived scheduled prompt is an unversioned copy of the knowledge file. It goes stale silently, and unlike the knowledge file nothing prompts anyone to re-read it. Any fact worth putting in a recurring brief should either be re-derived at run time or carry the date it was true.

Dries disabled this task on 26 Aug 2026, rather than patching the prompt. Two of the six defects above justified that on their own: the brief planned around a channel discontinued the same day, and its standing “do not flag Uitgaande klik” instruction was actively suppressing a live conversion finding every week. A review that reports on a dead channel while silencing a real one is worse than no review.

This is Dries’ report, not a verified state. It is a local Cowork task, and no Claude session can read local tasks — the registry returns cloud tasks only, so nothing here can confirm it is off. The next Monday it would have fired was 31 Aug.

If it is ever re-enabled, the prompt still needs the six repairs above — disabling froze the staleness, it did not fix it. The durable lesson stands regardless: a long-lived scheduled prompt is an unversioned copy of this file, and it goes stale silently.

Source: full prompt supplied by Dries 25 Aug 2026, read against this knowledge file; figures re-checked 26 Aug 2026